Тази публикация е част от поредица от статии за целите на редовете. Можете да намерите първата част тук:
- Част 1:Задаване и идентифициране на цели на редове
Сравнително добре известно е, че се използва TOP
или FAST n
намек за заявка може да зададе цел на ред в план за изпълнение (вижте Задаване и идентифициране на цели на ред в плановете за изпълнение, ако имате нужда от опресняване на целите на редовете и техните причини). Доста по-рядко се оценява, че полусъединяванията (и антисъединяванията) също могат да въведат цел на ред, въпреки че това е малко по-малко вероятно, отколкото е случаят с TOP
, FAST
и SET ROWCOUNT
.
Тази статия ще ви помогне да разберете кога и защо полусъединяването извиква логиката на целта на реда на оптимизатора.
Полусъединяване
Полусъединяване връща ред от един вход за свързване (A), ако има поне един съвпадащ ред на другия вход за свързване (B).
Съществените разлики между полусъединяване и редовно присъединяване са:
- Полусъединяването или връща всеки ред от вход A, или не. Не може да възникне дублиране на редове.
- Обикновеното свързване дублира редове, ако има няколко съвпадения в предиката за свързване.
- Полусъединяването е дефинирано да връща само колони от вход A.
- Регулярното свързване може да връща колони от единия (или и от двата) входа за свързване.
Понастоящем T-SQL няма поддръжка за директен синтаксис като FROM A SEMI JOIN B ON A.x = B.y
, така че трябва да използваме непреки форми като EXISTS
, SOME/ANY
(включително еквивалентната стенография IN
за сравнения на равенство) и задайте INTERSECT
.
Описанието на полусъединяване по-горе естествено намеква за прилагането на цел на ред, тъй като ние се интересуваме от намирането на който и съвпадащ ред в B, а не всички такива редове . Въпреки това, логическото полусъединяване, изразено в T-SQL, може да не доведе до план за изпълнение, използващ цел на ред поради няколко причини, които ще разопаковаме по-нататък.
Трансформация и опростяване
Логическото полу присъединяване може да бъде опростено или заменено с нещо друго по време на компилирането и оптимизацията на заявката. Примерът на AdventureWorks по-долу показва, че полусъединяването е премахнато изцяло поради доверена връзка с външен ключ:
ИЗБЕРЕТЕ TH.ProductID ОТ Production.TransactionHistory КАТО THWHERE TH.ProductID IN( ИЗБЕРЕТЕ P.ProductID ОТ Production.Product AS P);
Външният ключ гарантира, че Product
редове винаги ще съществуват за всеки ред История. В резултат на това планът за изпълнение има достъп само до TransactionHistory
таблица:
По-често срещан пример се вижда, когато полусъединението може да бъде трансформирано във вътрешно съединение. Например:
ИЗБЕРЕТЕ P.ProductID ОТ Production.Product AS P, КЪДЕ СЪЩЕСТВУВА( SELECT * FROM Production.ProductInventory AS INV WHERE INV.ProductID =P.ProductID);
Планът за изпълнение показва, че оптимизаторът е въвел агрегат (групиране на INV.ProductID
), за да се гарантира, че вътрешното присъединяване може да връща само Product
редове веднъж или изобщо (както се изисква за запазване на семантиката на полусъединяване):
Трансформацията към вътрешно свързване се проучва рано, тъй като оптимизаторът знае повече трикове за вътрешни equijoins, отколкото за полусъединения, което потенциално води до повече възможности за оптимизация. Естествено, окончателният избор на план все още е решение, основано на разходите, сред изследваните алтернативи.
Ранни оптимизации
Въпреки че в T-SQL липсва директно SEMI JOIN
синтаксис, оптимизаторът знае всичко за полусъединяванията и може да ги манипулира директно. Обикновените заобиколни синтаксиси на полуприсъединяване се трансформират в "реално" вътрешно полусъединяване в началото на процеса на компилиране на заявка (много преди да се разгледа дори тривиален план).
Двете основни синтактични групи за заобикаляне са EXISTS/INTERSECT
и ANY/SOME/IN
. EXISTS
и INTERSECT
случаите се различават само по това, че последният идва с имплицитно DISTINCT
(групиране на всички проектирани колони). И двете EXISTS
и INTERSECT
се анализират като EXISTS
с корелирана подзаявка. ANY/SOME/IN
всички представяния се интерпретират като НЯКОЙ операция. Можем да проучим рано тази оптимизационна активност с няколко недокументирани флага за проследяване, които изпращат информация за активността на оптимизатора до раздела SSMS съобщения.
Например, полусъединяването, което използвахме досега, също може да бъде написано с IN
:
ИЗБЕРЕТЕ P.ProductIDFROM Production.Product AS PWHERE P.ProductID IN /* or =ANY/SOME */( SELECT TH.ProductIDFROM Production.TransactionHistory КАТО TH)ОПЦИЯ (QUERYTRACEON 3604, QUERYTRACEON 8606, QUERY2118); /предварително>Дървото за въвеждане на оптимизатора е както следва:
Скаларният оператор ScaOp_SomeComp е
SOME
сравнение, споменато малко по-горе. 2 е кодът за тест за равенство, тъй катоIN
е еквивалентно на= SOME
. Ако се интересувате, има кодове от 1 до 6, представляващи съответно (<, =, <=,>, !=,>=) оператори за сравнение.Връщане към
EXISTS
синтаксис, който предпочитам да използвам най-често, за да изразя непряко полусъединяване:ИЗБЕРЕТЕ P.ProductIDFROM Production.Product AS PWHERE EXISTS( SELECT * FROM Production.TransactionHistory КАТО TH WHERE TH.ProductID =P.ProductID)ОПЦИЯ (QUERYTRACEON 3604, QUERYTRACEON 8606, QUERYTRACEON 862);Входното дърво на оптимизатора е:
Това дърво е доста директен превод на текста на заявката; все пак имайте предвид, че
SELECT *
вече е заменен от проекция на постоянната целочислена стойност 1 (вижте предпоследния ред на текста).Следващото нещо, което оптимизаторът прави, е да деактивира подзаявката в релационната селекция (=филтър) с помощта на правилото RemoveSubqInSel . Оптимизаторът винаги прави това, тъй като не може да работи директно с подзаявки. Резултатът е прилагане (известно още като корелирано или странично съединение):
(Същото правило за премахване на подзаявка произвежда същия изход за
SOME
входно дърво също).Следващата стъпка е да пренапишете приложението като обикновено присъединяване с помощта на ApplyHandler управляват семейство. Това е нещо, което оптимизаторът винаги се опитва да направи, защото има повече правила за изследване за присъединяване, отколкото за прилагане. Не всяко приложение може да бъде пренаписано като присъединяване, но настоящият пример е ясен и е успешен:
Обърнете внимание, че типът на съединението е ляво полу. Всъщност това е точно същото дърво, което бихме получили веднага, ако T-SQL поддържа синтаксис като:
ИЗБЕРЕТЕ P.ProductID ОТ Production.Product AS P LEFT SEMI JOIN Production.TransactionHistory КАТО TH ON TH.ProductID =P.ProductID;Би било хубаво да можете да изразявате запитвания по-директно по този начин. Както и да е, заинтересованият читател се насърчава да проучи горните дейности за опростяване с други логически еквивалентни начини за писане на това полусъединение в T-SQL.
Важният извод на този етап е, че оптимизаторът винаги премахва подзаявки , като ги замените с приложение. След това се опитва да пренапише приложението като редовно присъединяване, за да увеличи максимално шансовете за намиране на добър план. Не забравяйте, че всичко преди това се случва преди да бъде разгледан дори тривиален план. По време на оптимизация, базирана на разходите, оптимизаторът може също да обмисли трансформация на присъединяване обратно към приложение.
Хеширане и семи присъединяване
SQL Server има три основни опции за физически реализации, налични за логическо полусъединяване. Докато е налице предикат за равносъединение, са налични хеш и обединяване; и двете могат да работят в режим на ляво и дясно полусъединяване. Съединяването с вложени цикли поддържа само ляво (не дясно) полусъединяване, но не изисква предикат за равносъединение. Нека разгледаме физическите опции за хеширане и сливане за нашата примерна заявка (написана като пресичане на набор този път):
ИЗБЕРЕТЕ P.ProductID ОТ Production.Product AS PINTERSECTSELECT TH.ProductID FROM Production.TransactionHistory КАТО TH;Оптимизаторът може да намери план за всичките четири комбинации от (ляво/дясно) и (хеш/сливане) полусъединяване за тази заявка:
Струва си да се спомене накратко защо оптимизаторът може да разгледа както лявото, така и дясното полусъединяване за всеки тип присъединяване. За хеш полусъединяване, основно съображение за разходите е прогнозният размер на хеш таблицата, която първоначално винаги е левият (горен) вход. За полусъединяване при сливане свойствата на всеки вход определят дали ще се използва едно към много или по-малко ефективно сливане много към много с работна таблица.
От горните планове за изпълнение може да е видно, че нито хеширане, нито полусъединяване с сливане биха имали полза от задаване на цел за ред . И двата типа на свързване винаги тестват предиката за свързване в самото присъединяване и се стремят да консумират всички редове от двата входа, за да върнат пълен набор от резултати. Това не означава, че оптимизации на производителността не съществуват за хеширане и обединяване като цяло – например и двете могат да използват растерни изображения, за да намалят броя на редовете, достигащи до присъединяването. По-скоро въпросът е, че целта на ред на който и да е вход няма да направи хеширането или полусъединяването с сливане по-ефективно.
Вложени цикли и прилагане на полуприсъединяване
Оставащият тип физическо свързване е вложени цикли, които се предлагат в два варианта:редовни (некорелирани) вложени цикли и прилагане вложени цикли (понякога наричани също корелирани или странично присъединете се).
Редовното присъединяване с вложени цикли е подобно на присъединяването с хеширане и сливане, тъй като предикатът за присъединяване се оценява при присъединяването. Както и преди, това означава, че няма стойност при задаване на цел за ред за нито един от входните данни. Левият (горен) вход винаги ще бъде напълно изразходван в крайна сметка и вътрешният вход няма начин да определи кой ред (редове) трябва да бъде приоритизиран, тъй като не можем да знаем дали един ред ще се присъедини или не, докато предикатът не бъде тестван при присъединяването .
Обратно, присъединяването на прилагане на вложени цикли има една или повече външни препратки (корелирани параметри) при присъединяването, с предикат за присъединяване, натиснат надолу вътрешната (долната) страна на съединението. Това създава възможност за полезно приложение на цел на ред. Припомнете си, че полусъединяването изисква само да проверим за съществуването на ред на вход за присъединяване B, който съвпада с текущия ред на вход за присъединяване A (мисляйки само за стратегиите за присъединяване на вложени цикли сега).
С други думи, при всяка итерация на приложение можем да спрем да гледаме на вход B веднага щом бъде намерено първото съвпадение, като използваме предиката за присъединяване, натиснат надолу. Точно това е нещото, за което целта на ред е добра:генериране на част от план, оптимизиран за бързо връщане на първите n съвпадащи реда (където
n = 1
тук).Разбира се, гол от ред може да бъде нещо добро или не, в зависимост от обстоятелствата. В това отношение няма нищо особено в гола на полусъединяване. Помислете за ситуация, при която вътрешната страна на полусъединяването е по-сложна от един прост достъп до таблица, може би свързване с няколко таблици. Задаването на цел за ред може да помогне на оптимизатора да избере ефективна навигационна стратегия само за това конкретно поддърво , намиране на първия съвпадащ ред, който да удовлетвори полусъединяването чрез вложени цикли, свързвания и търсене на индекс. Без целта за ред, оптимизаторът може естествено да избере хеш или обединяване с обединяване със сортиране, за да сведе до минимум очакваните разходи за връщане на всички възможни редове. Имайте предвид, че тук има предположение, а именно, че хората обикновено пишат полусъединения с очакването, че ред, съответстващ на условието за търсене, всъщност съществува. Това ми се струва достатъчно справедливо предположение.
Независимо от това, важният момент на този етап е:Само прилагане присъединяването на вложените цикли има цел на реда прилага се от оптимизатора (все пак не забравяйте, че цел на ред за присъединяване на вложени цикли се добавя само ако целта на реда е по-малка от оценката без нея). Ще разгледаме няколко работещи примера, за да се надяваме, че всичко това е ясно по-нататък.
Примери за полусъединяване на вложени цикли
Следващият скрипт създава две временни таблици на купчина. Първият има числа от 1 до 20 включително; другият има 10 копия от всяко число в първата таблица:
ПРОСТАНЕ ТАБЛИЦА, АКО СЪЩЕСТВУВА #E1, #E2; СЪЗДАВАНЕ НА ТАБЛИЦА #E1 (c1 цяло число NULL);СЪЗДАВАНЕ НА ТАБЛИЦА #E2 (c1 цяло число NULL); INSERT #E1 (c1)SELECT SV.numberFROM master.dbo.spt_values КАТО SVWHERE SV.[type] =N'P' И SV.number>=1 И SV.number <=20; INSERT #E2 (c1)SELECT (SV.number % 20) + 1FROM master.dbo.spt_values КАТО SVWHERE SV.[type] =N'P' И SV.number>=1 И SV.number <=200;предварително>Без индекси и относително малък брой редове, оптимизаторът избира реализация на вложени цикли (вместо хеширане или сливане) за следната заявка за полу присъединяване). Недокументираните флагове за проследяване ни позволяват да видим изходното дърво и информация за целта на оптимизатора:
ИЗБЕРЕТЕ E1.c1 ОТ #E1 КАТО E1, КЪДЕТО E1.c1 IN (ИЗБЕРЕТЕ E2.c1 ОТ #E2 КАТО E2) ОПЦИЯ (QUERYTRACEON 3604, QUERYTRACEON 8607, QUERYTRACEON 8612);Приблизителният план за изпълнение включва полусъединяване с вложени цикли, с 200 реда на пълно сканиране на таблица
#E2
. 20-те итерации на цикъла дават обща оценка от 4000 реда:
Свойствата на оператора на вложените цикли показват, че предикатът се прилага при присъединяването което означава, че това е съединяване на некорелирани вложени цикли :
Изходният флаг за проследяване (в раздела SSMS съобщения) показва полусъединяване с вложени цикли и без цел на ред (RowGoal 0):
Имайте предвид, че планът след изпълнение за тази заявка за играчка няма да покаже общо 4000 реда, прочетени от таблица #E2. Полусъединяването на вложените цикли (корелирани или не) ще спрат да търсят повече редове от вътрешната страна (на итерация), веднага щом се срещне първото съвпадение за текущия външен ред. Сега редът на редовете, срещани при сканирането на купчина на #E2 при всяка итерация, е недетерминиран (и може да е различен при всяка итерация), така че по принцип почти всички редове могат да бъдат тествани при всяка итерация, в случай че съвпадащият ред се срещне възможно най-късно (или наистина, в случай на липса на съвпадащ ред, изобщо).
Например, ако приемем изпълнение по време на изпълнение, при което редовете се сканират в един и същ ред (напр. "ред за вмъкване") всеки път, общият брой на сканираните редове в този пример за играчка ще бъде 20 реда при първата итерация, 1 ред на втората итерация, 2 реда на третото повторение и така нататък за общо 20 + 1 + 2 + (...) + 19 =210 реда. Наистина е много вероятно да наблюдавате това общо, което казва повече за ограниченията на простия демонстрационен код, отколкото за всичко друго. Човек не може да разчита на реда на редовете, върнати от неподреден метод за достъп, както и не може да разчита на очевидно подреден изход от заявка без
ORDER BY
от най-високо ниво клауза.Прилагане на полуприсъединяване
Сега създаваме неклъстериран индекс в по-голямата таблица (за да насърчим оптимизатора да избере прилагане на полуприсъединяване) и стартираме заявката отново:
СЪЗДАВАНЕ НА НЕКЛУСТРИРАН ИНДЕКС nc1 НА #E2 (c1); ИЗБЕРЕТЕ E1.c1 ОТ #E1 КАТО E1, КЪДЕТО E1.c1 IN (ИЗБЕРЕТЕ E2.c1 ОТ #E2 КАТО E2) ОПЦИЯ (QUERYTRACEON 3604, QUERYTRACEON 8607, QUERYTRACEON 8612);Планът за изпълнение вече включва прилагане на полусъединяване, с 1 ред на търсене на индекс (и 20 итерации както преди):
Можем да кажем, че това е прилагане на полуприсъединяване защото свойствата на присъединяване показват външна препратка вместо предикат за присъединяване:
Предикатът за присъединяване е избутан надолу вътрешната страна на приложението и съответства на новия индекс:
Очаква се всяко търсене да върне 1 ред, въпреки факта, че всяка стойност се дублира 10 пъти в тази таблица; това е ефект на ред цела . Целта на реда ще бъде по-лесна за идентифициране при компилации на SQL Server, които разкриват EstimateRowsWithoutRowGoal план атрибут (SQL Server 2017 CU3 към момента на писане). В предстояща версия на Plan Explorer това също ще бъде изложено в подсказките за съответните оператори:
Изходният флаг за проследяване е:
Физическият оператор е променен от свързване на цикли към приложение, работещо в режим на ляво полусъединяване. Достъп до таблица
#E2
е придобил цел на ред 1 (кардиналността без целта на реда е показана като 10). Целта на реда не е голяма работа в този случай, тъй като цената за извличане на приблизително десет реда на търсене не е много повече, отколкото за един ред. Деактивиране на целите на редовете за тази заявка (използвайки флаг за проследяване 4138 илиDISABLE_OPTIMIZER_ROWGOAL
намек за заявка) няма да промени формата на плана.Независимо от това, при по-реалистични заявки, намаляването на разходите поради целта на вътрешния ред може да направи разликата между конкуриращите се опции за изпълнение. Например, деактивирането на целта на реда може да накара оптимизатора да избере вместо това хеш или полусъединяване за сливане или някоя от многото други опции, разглеждани за заявката. Ако не друго, целта на реда тук отразява точно факта, че прилагането на полусъединяване ще спре търсенето във вътрешната страна веднага щом бъде намерено първото съвпадение и ще премине към следващия външен страничен ред.
Имайте предвид, че дубликатите са създадени в таблица
#E2
така че целта за прилагане на полусъединен ред (1) ще бъде по-ниска от нормалната оценка (10, от информация за плътността на статистиката). Ако нямаше дубликати, оценката на реда за всяко търсене в#E2
също ще бъде 1 ред, така че целта за ред от 1 няма да се прилага (запомнете общото правило за това!)Цели на ред срещу горни
Като се има предвид, че плановете за изпълнение изобщо не показват наличието на цел за ред преди SQL Server 2017 CU3, може да се помисли, че би било по-ясно да се приложи тази оптимизация с помощта на изричен оператор Top, а не скрито свойство като цел на ред. Идеята би била просто да се постави оператор Top (1) от вътрешната страна на прилагащо полу/анти присъединяване, вместо да се задава цел на ред в самото съединение.
Използването на Top оператор по този начин не би било напълно безпрецедентно. Например, вече има специална версия на Top, известна като отгоре на броя на редовете, която се вижда в плановете за изпълнение на модификация на данни, когато
SET ROWCOUNT
е различен от нула е в сила (обърнете внимание, че това конкретно използване е остаряло от 2005 г., въпреки че все още е разрешено в SQL Server 2017). Внедряването на върха на броя на редовете е малко тромаво, тъй като операторът top винаги се показва като Top (0) в плана за изпълнение, независимо от действителното ограничение за броя на редовете, което е в сила.Няма убедителна причина целта на ред за прилагане на полуприсъединяване да не може да бъде заменена с изричен оператор Top (1). Въпреки това има някои причини да предпочитате да не правите това:
- Добавянето на изрична горна част (1) изисква повече усилия за кодиране и тестване на оптимизатора, отколкото добавянето на цел на ред (която вече се използва за други неща).
- Top не е релационен оператор; оптимизаторът има малка подкрепа за разсъждения за това. Това може да повлияе негативно на качеството на плана, като ограничава способността на оптимизатора да трансформира части от план за заявка, напр. чрез преместване на агрегати, съюзи, филтри и съединения наоколо.
- Това ще въведе тясна връзка между прилагането на прилагането на полусъединяването и горната част. Специалните случаи и тясното свързване са чудесни начини за въвеждане на грешки и за затрудняване на бъдещите промени и по-податливи на грешки.
- Върхът (1) би бил логически излишен и ще присъства само за страничния ефект на целта на реда.
Тази последна точка си струва да се разшири с пример:
ИЗБЕРЕТЕ P.ProductID ОТ Production.Product КАТО PWHERE СЪЩЕСТВУВА ( ИЗБЕРЕТЕ ВЪРХА (1) TH.ProductID ОТ Production.TransactionHistory КАТО TH WHERE TH.ProductID =P.ProductID );
TOP (1)
в съществуващата подзаявка се опростява от оптимизатора, като дава прост план за изпълнение на полусъединяване:
Оптимизаторът може също да премахне излишен DISTINCT
или GROUP BY
в подзаявката. Всички по-долу произвеждат същия план като по-горе:
-- Излишно DISTINCTSELECT P.ProductID ОТ Production.Product AS PWHERE EXISTS ( SELECT DISTINCT TH.ProductID FROM Production.TransactionHistory КАТО TH WHERE TH.ProductID =P.ProductID); -- Излишна група BYSELECT P.ProductID ОТ Production.Product AS PWHERE EXISTS ( SELECT TH.ProductID FROM Production.TransactionHistory КАТО TH WHERE TH.ProductID =P.ProductID GROUP BY TH.ProductID); -- Излишно DISTINCT TOP (1)ИЗБЕРЕТЕ P.ProductID ОТ Производство.Продукт КАТО PWHERE СЪЩЕСТВУВА (ИЗБЕРЕТЕ DISTINCT TOP (1) TH.ProductID ОТ Production.TransactionHistory КАТО TH, КЪДЕТО TH.ProductID =P.ProductID);
Обобщение и заключителни мисли
Самоприлагайте вложените цикли semi join могат да имат цел за ред, зададена от оптимизатора. Това е единственият тип на присъединяване, който избутва предиката(ите) на присъединяване надолу от съединението, което позволява тестване за съществуване на съвпадение да се извърши ранно . Некорелирани вложени цикли, полусъединяващи се почти никога* задава цел за ред и не прави хеширане или полусъединение. Прилагането на вложени цикли може да се разграничи от некорелирани вложени цикли, присъединени по наличието на външни препратки (вместо предикат) на оператора за присъединяване на вложени цикли за приложение.
Шансовете да видите приложно полуприсъединяване в крайния план за изпълнение до известна степен зависят от дейността по ранна оптимизация. При липса на директен T-SQL синтаксис, трябва да изразяваме полусъединения в косвени термини. Те се анализират в логическо дърво, съдържащо подзаявка, която активността на ранния оптимизатор се трансформира в прилагане и след това в некорелирано полусъединяване, където е възможно.
Тази дейност по опростяване определя дали логическото полусъединяване се представя на базирания на разходите оптимизатор като прилагане или обикновено полусъединяване. Когато е представен като логичноприложи semi join, CBO е почти сигурно, че ще изготви окончателен план за изпълнение, включващ вложени цикли за физическо прилагане (и така задаване на цел за ред). Когато е представен с некорелирано полусъединяване, CBO може помислете за трансформация в приложение (или може да не). Окончателният избор на план е серия от решения, базирани на разходите, както обикновено.
Подобно на всички редови голове, голът на полусъединен ред може да бъде добро или лошо нещо за представянето. Знаейки, че прилагането на полусъединяване поставя цел за ред, поне ще помогне на хората да разпознаят и да се справят с причината, ако възникне проблем. Решението не винаги (или дори обикновено) ще бъде деактивиране на целите на редовете за заявката. Често могат да бъдат направени подобрения в индексирането (и/или заявката), за да се осигури ефективен начин за намиране на първия съвпадащ ред.
Ще разгледам антиполусъединяванията в отделна статия, продължавайки поредицата от цели.
* Изключението е некорелирани вложени цикли, полусъединяване без предикат без присъединяване (необичайна гледка). Това поставя цел за ред.