Това е част от серията проблемни оператори за вътрешните оператори на SQL. За да прочетете първата публикация, щракнете тук.
SQL Server съществува повече от 30 години и аз работя със SQL Server почти толкова дълго. Виждал съм много промени през годините (и десетилетия!) и версии на този невероятен продукт. В тези публикации ще споделя с вас как гледам на някои от функциите или аспектите на SQL Server, понякога заедно с малко историческа перспектива.
Последния път говорих за операция за сканиране в план за заявка на SQL Server като потенциално проблематичен оператор в диагностиките на SQL Server. Въпреки че сканирането често се използва само защото няма полезен индекс, има моменти, когато сканирането всъщност е по-добър избор от операция за търсене на индекс.
В тази статия ще ви разкажа за друго семейство оператори, което понякога се разглежда като проблематично:хеширането. Хеширането е много добре познат алгоритъм за обработка на данни, който съществува от много десетилетия. Изучавах го в моите класове по структури от данни още, когато за първи път изучавах компютърни науки в университета. Ако искате основна информация за хеширането и хеш функциите, можете да разгледате тази статия в Wikipedia. Въпреки това, SQL Server не добави хеширане към своя репертоар от опции за обработка на заявки до SQL Server 7. (Като настрана ще спомена, че SQL Server наистина използва хеширане в някои от собствените си вътрешни алгоритми за търсене. Както се споменава в статията в Wikipedia , хеширането използва специална функция за съпоставяне на данни с произволен размер към данни с фиксиран размер. SQL използва хеширане като техника за търсене, за да преобразува всяка страница от база данни с произволен размер в буфер в паметта, който е с фиксиран размер. , преди имаше опция за sp_configure наречени „хеш кофи“, които ви позволяват да контролирате броя на кофи, използвани за хеширане на страници от база данни в буфери на паметта.)
Какво е хеширане?
Хеширането е техника за търсене, която не изисква подреждането на данните. SQL Server може да го използва за операции JOIN, операции за агрегиране (DISTINCT или GROUP BY) или операции UNION. Общото между тези три операции е, че по време на изпълнение машината за заявки търси съвпадащи стойности. В JOIN искаме да намерим редове в една таблица (или набор от редове), които имат съвпадащи стойности с редове в друга. (И да, знам за обединения, които не сравняват редове въз основа на равенство, но тези неравнопоставени съединения са без значение за тази дискусия.) За GROUP BY намираме съвпадащи стойности, които да включим в същата група, а за UNION и DISTINCT, търсим съвпадащи стойности, за да ги изключим. (Да, знам, че UNION ALL е изключение.)
Преди SQL Server 7 единственият начин, по който тези операции можеха лесно да намират съответстващи стойности, беше, ако данните бяха сортирани. Така че, ако няма съществуващ индекс, който поддържа данните в сортиран ред, планът на заявката ще добави операция SORT към плана. Хеширането организира вашите данни за ефективно търсене, като постави всички редове, които имат същия резултат от вътрешната хеш функция, в една и съща „хеш кофа“.
За по-подробно обяснение на хеш операцията JOIN на SQL Server, включително диаграми, разгледайте тази публикация в блога от SQL Shack.
След като хеширането стана опция, SQL Server не пренебрегна напълно възможността за сортиране на данни преди присъединяване или агрегиране, но просто стана възможност за обмисляне на оптимизатора. Като цяло обаче, ако се опитвате да се присъедините, агрегирате или изпълните UNION върху несортирани данни, оптимизаторът обикновено избира хеш операция. Толкова много хора приемат, че HASH JOIN (или друга HASH операция) в план означава, че нямате подходящи индекси и че трябва да създадете подходящи индекси, за да избегнете хеш операцията.
Нека разгледаме един пример. Първо ще създам две неиндексирани таблици.
USE AdventureWorks2016 GO DROP TABLE IF EXISTS Details;
GO
SELECT * INTO Details FROM Sales.SalesOrderDetail;
GO
DROP TABLE IF EXISTS Headers;
GO
SELECT * INTO Headers FROM Sales.SalesOrderHeader;
GO
Now, I’ll join these two tables together and filter the rows in the Details table:
SELECT *
FROM Details d JOIN Headers h
ON d.SalesOrderID = h.SalesOrderID
WHERE SalesOrderDetailID < 100;
Quest Spotlight Tuning Pack изглежда не показва хеш присъединяването като проблем. Той подчертава само двете сканирания на таблицата.
Предложенията препоръчват изграждането на индекс за всяка таблица, която включва всяка отделна неключова колона като Включена колона. Рядко приемам тези препоръки (както споменах в предишната си публикация). Ще създам само индекса върху Подробности таблица, в колоната за присъединяване и няма включени колони.
CREATE INDEX Header_index on Headers(SalesOrderID)
;
След като този индекс бъде изграден, HASH JOIN изчезва. Индексът сортира данните в Заглавките таблица и позволява на SQL Server да намери съвпадащите редове във вътрешната таблица, използвайки последователността на сортиране на индекса. Сега най-скъпата част от плана е сканирането на външната маса (Подробности ), който може да бъде намален чрез изграждане на индекс върху SalesOrderID колона в тази таблица. Ще оставя това като упражнение за читателя.
Въпреки това, план с HASH JOIN не винаги е нещо лошо. Алтернативният оператор (освен в специални случаи) е ПРИСЪЕДИНЯВАНЕ НА ВЛОЖЕНИ ЛИКВИ и това обикновено е изборът, когато има добри индекси. Въпреки това, операция NESTED цикли изисква множество търсения на вътрешната таблица. Следният псевдокод показва алгоритъма за присъединяване на вложени цикли:
for each row R1 in the outer table
for each row R2 in the inner table
if R1 joins with R2
return (R1, R2)
Както подсказва името, ПРИСЪЕДИНЯВАНЕТО НА ВГЛАЖЕН БРИКЛ се изпълнява като вложен цикъл. Търсенето на вътрешната таблица обикновено ще се извършва многократно, веднъж за всеки квалифициращ ред във външната таблица. Дори ако има само няколко процента от редовете, отговарящи на изискванията, ако таблицата е много голяма (може би в стотици милиони, милиарди или редове), ще има много редове за четене. В система, която е свързана с I/O, тези милиони или милиарди четения могат да бъдат истинско затруднение.
HASH JOIN, от друга страна, не извършва многократно четене на нито една таблица. Той чете външната таблица веднъж, за да създаде хеш кофите, и след това чете вътрешната таблица веднъж, проверявайки хеш кофите, за да види дали има съвпадащ ред. Имаме горна граница от едно преминаване през всяка таблица. Да, има ресурси на процесора, необходими за изчисляване на хеш функцията и управление на съдържанието на кофите. Има ресурси за памет, необходими за съхраняване на хешираната информация. Но ако имате I/O свързана система, може да имате свободна памет и ресурси на процесора. HASH JOIN може да бъде разумен избор за оптимизатора в тези ситуации, когато вашите I/O ресурси са ограничени и вие се присъединявате към много големи таблици.
Ето псевдокод за алгоритъма за хеш присъединяване:
for each row R1 in the build table
begin
calculate hash value on R1 join key(s)
insert R1 into the appropriate hash bucket
end
for each row R2 in the probe table
begin
calculate hash value on R2 join key(s)
for each row R1 in the corresponding hash bucket
if R1 joins with R2
output (R1, R2)
end
Както бе споменато по-рано, хеширането може да се използва и за операции за агрегиране (както и UNION). Отново, ако има полезен индекс, който вече има сортирани данни, групирането на данните може да се извърши много ефективно. Има обаче и много ситуации, в които хеширането изобщо не е лош оператор. Помислете за заявка като следната, която групира данните в Подробности таблица (създадена по-горе) от ProductID колона. В таблицата има 121 317 реда и само 266 различни ProductID стойности.
SELECT ProductID, count(*)
FROM Details
GROUP BY ProductID;
GO
Използване на операции за хеширане
За да използва хеширане, SQL Server трябва само да създаде и поддържа 266 кофи, което не е много. Всъщност Quest Spotlight Tuning Pack не показва, че има някакви проблеми с тази заявка.
Да, трябва да направи сканиране на таблица, но това е, защото трябва да проучим всеки ред в таблицата и знаем, че сканирането не винаги е лошо нещо. Индексът би помогнал само за предварителното сортиране на данните, но използването на хеш агрегация за такъв малък брой групи все пак обикновено ще даде разумна производителност дори и без наличен полезен индекс.
Подобно на сканирането на таблици, операциите по хеширане често се разглеждат като „лош“ оператор, който трябва да има в план. Има случаи, в които можете значително да подобрите производителността, като добавите полезни индекси за премахване на хеш операциите, но това не винаги е вярно. И ако се опитвате да ограничите броя на индексите на таблици, които са силно актуализирани, трябва да сте наясно, че хеш операциите не винаги са нещо, което трябва да бъде „поправено“, така че оставянето на заявката за използване на хеш може да бъде разумно нещо да направя. В допълнение, за определени заявки на големи таблици, работещи на I/O свързани системи, хеширането всъщност може да даде по-добра производителност от алтернативните алгоритми поради ограничения брой четения, които трябва да се извършат. Единственият начин да знаете със сигурност е да тествате различни възможности във вашата система, с вашите заявки и вашите данни.
В следващата публикация от тази серия ще ви разкажа за други проблемни оператори, които може да се появят във вашите планове за заявка, така че проверете отново скоро!