Database
 sql >> база данни >  >> RDS >> Database

Ограничения на оптимизатора с филтрирани индекси

Един от случаите на използване на филтриран индекс, споменати в Books Online, се отнася до колона, която съдържа предимно NULLs стойности. Идеята е да се създаде филтриран индекс, който изключва NULLs , което води до по-малък неклъстериран индекс, който изисква по-малко поддръжка от еквивалентния нефилтриран индекс. Друго популярно използване на филтрирани индекси е филтрирането на NULLs от UNIQUE индекс, даващ поведението, което потребителите на други машини за бази данни могат да очакват от UNIQUE по подразбиране индекс или ограничение:уникалността се прилага само за не-NULLs стойности.

За съжаление, оптимизаторът на заявки има ограничения по отношение на филтрираните индекси. Тази публикация разглежда няколко по-малко известни примера.

Примерни таблици

Ще използваме две таблици (A и B), които имат една и съща структура:сурогатен клъстериран първичен ключ, предимно NULLs колона, която е уникална (без внимание на NULLs ) и колона за допълване, която представлява другите колони, които може да са в реална таблица.

Колоната, която представлява интерес е предимно-NULLs един, който съм декларирал като SPARSE . Рядката опция не е задължителна, просто я включвам, защото нямам много шанс да я използвам. Във всеки случай SPARSE вероятно има смисъл в много сценарии, при които се очаква данните в колоната да бъдат предимно NULLs . Чувствайте се свободни да премахнете атрибута sparse от примерите, ако желаете.

CREATE TABLE dbo.TableA( pk integer IDENTITY PRIMARY KEY, data bigint SPARSE NULL, padding binary(250) NOT NULL DEFAULT 0x); CREATE TABLE dbo.TableB( pk integer IDENTITY PRIMARY KEY, data bigint SPARSE NULL, padding binary(250) NOT NULL DEFAULT 0x);

Всяка таблица съдържа числата от 1 до 2000 в колоната с данни с допълнителни 40 000 реда, където колоната с данни е NULLs :

-- Числа 1 - 2,000 INSERT dbo.TableA WITH (TABLOCKX) (data)SELECT TOP (2000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))FROM sys.columns КАТО cCROSS JOIN sys.columns КАТО c2ORDERBY ROW_NUMBER() НАД (ПОРЪЧАЙТЕ ПО (ИЗБЕРЕТЕ NULL)); -- NULLsINSERT TOP (40000) dbo.TableA WITH (TABLOCKX) (data)SELECT CONVERT(bigint, NULL)FROM sys.columns КАТО cCROSS JOIN sys.columns КАТО c2; -- Копирайте в TableBINSERT dbo.TableB WITH (TABLOCKX) (data)SELECT ta.dataFROM dbo.TableA AS ta;

И двете таблици получават UNIQUE филтриран индекс за 2000 не-NULLs стойности на данните:

СЪЗДАЙТЕ УНИКАЛЕН НЕКЛУСТРИРАН ИНДЕКС uqAON dbo.TableA (данни) КЪДЕТО данните НЕ СА NULL; СЪЗДАЙТЕ УНИКАЛЕН НЕКЛУСТРИРАН ИНДЕКС uqBON dbo.TableB (данни), КЪДЕТО данните НЕ СА NULL;

Изходът на DBCC SHOW_STATISTICS обобщава ситуацията:

DBCC SHOW_STATISTICS (TableA, uqA) С STAT_HEADER;DBCC SHOW_STATISTICS (TableB, uqB) С STAT_HEADER;

Примерна заявка

Заявката по-долу извършва просто свързване на двете таблици – представете си, че таблиците са в някаква връзка родител-дете и много от външните ключове са NULL. Все пак нещо в този смисъл.

ИЗБЕРЕТЕ ta.data, tb.dataFROM dbo.TableA КАТО taJOIN dbo.TableB AS tb ON ta.data =tb.data;

План за изпълнение по подразбиране

С SQL Server в неговата конфигурация по подразбиране, оптимизаторът избира план за изпълнение, включващ присъединяване на паралелно вложени цикли:

Този план е с приблизителна цена от 7,7768 magic optimizer units™.

В този план обаче има някои странни неща. Търсенето на индекс използва нашия филтриран индекс в таблица B, но заявката се управлява от клъстерирано индексно сканиране на таблица A. Предикатът за присъединяване е тест за равенство на колоните с данни, който отхвърля NULLs (независимо от ANSI_NULLS настройка). Може да се надяваме, че оптимизаторът ще изпълни някои разширени разсъждения въз основа на това наблюдение, но не. Този план чете всеки ред от таблица А (включително 40 000 NULLs) ), извършва търсене във филтрирания индекс на таблица B за всеки от тях, разчитайки на факта, че NULLs няма да съответства на NULLs в това търсене. Това е огромна загуба на усилия.

Странното е, че оптимизаторът трябва да е осъзнал, че присъединяването отхвърля NULLs за да избере филтрирания индекс за търсене на таблица B, но не се сети да филтрира NULLs първо от таблица А – или още по-добре, просто да сканирате NULLs -безплатен филтриран индекс в таблица А. Може да се чудите дали това е решение, базирано на разходите, може би статистиката не е много добра? Може би трябва да принудим използването на филтрирания индекс с намек? Намекването на филтрирания индекс в таблица A просто води до същия план с обърнати роли – сканиране на таблица B и търсене в таблица A. Принудителното използване на филтрирания индекс за двете таблици води до грешка 8622 :процесорът на заявки не може да създаде план за заявка.

Добавяне на предикат NOT NULL

Подозрения, че причината е нещо общо с подразбиращото се NULLs -отхвърляне на предиката за присъединяване, добавяме изрично NOT NULL предикат към ON клауза (или WHERE клауза, ако предпочитате, тук става дума за същото):

ИЗБЕРЕТЕ ta.data, tb.dataFROM dbo.TableA КАТО taJOIN dbo.TableB AS tb ON ta.data =tb.data И ta.data НЕ Е NULL;

Добавихме NOT NULL проверете колоната на таблица А, защото първоначалният план сканира клъстерирания индекс на тази таблица, вместо да използва нашия филтриран индекс (търсенето в таблица Б беше добре – използваше филтрирания индекс). Новата заявка е семантично същата като предишната, но планът за изпълнение е различен:

Сега имаме очакваното сканиране на филтрирания индекс в таблица А, което дава 2000 не-NULLs редове за задвижване на вложения цикъл търси в таблица Б. И двете таблици използват нашите филтрирани индекси очевидно оптимално сега:новият план струва само 0,362835 единици (надолу от 7,7768). Можем обаче да се справим по-добре.

Добавяне на два предиката NOT NULL

Излишният NOT NULL предикат за таблица А направи чудеса; какво ще стане, ако добавим и за таблица Б?

ИЗБЕРЕТЕ ta.data, tb.dataFROM dbo.TableA КАТО taJOIN dbo.TableB AS tb ON ta.data =tb.data И ta.data НЕ Е NULL И tb.data НЕ Е NULL;

Тази заявка все още е логически същата като двете предишни усилия, но планът за изпълнение отново е различен:

Този план изгражда хеш таблица за 2000-те реда от таблица А, след което проверява съвпаденията, използвайки 2000-те реда от таблица Б. Приблизителният брой върнати редове е много по-добър от предишен план (забелязахте ли оценката от 7 619 там?) и прогнозната цена за изпълнение отново спадна от 0,362835 на 0,0772056 .

Можете да опитате да наложите хеш присъединяване, като използвате намек за оригинала или единичен NOT NULL запитвания, но няма да получите евтиния план, показан по-горе. Оптимизаторът просто няма способността да разсъждава напълно за NULLs -отхвърляне на поведението на присъединяването, тъй като то се прилага към нашите филтрирани индекси без двата излишни предиката.

Позволено е да бъдете изненадани от това – дори ако това е само идеята, че един излишен предикат не е достатъчен (със сигурност ако ta.data е NOT NULL и ta.data = tb.data , следва, че tb.data също е NOT NULL , нали?)

Все още не е перфектно

Малко е изненадващо да видите хеш присъединяване там. Ако сте запознати с основните разлики между трите оператора за физическо свързване, вероятно знаете, че хеш присъединяването е най-добрият кандидат, където:

  1. Не е налично предварително сортирано въвеждане
  2. Входът за изграждане на хеш е по-малък от входния сонда
  3. Входът на сондата е доста голям

Нито едно от тези неща не е вярно тук. Нашето очакване би било, че най-добрият план за тази заявка и набор от данни ще бъде обединяване за сливане, което използва подредените входни данни, налични от нашите два филтрирани индекса. Можем да опитаме да намекнем за обединяване за сливане, като запазим двете допълнителни ON предикати на клауза:

ИЗБЕРЕТЕ ta.data, tb.dataFROM dbo.TableA КАТО taJOIN dbo.TableB AS tb ON ta.data =tb.data И ta.data НЕ СА NULL И tb.data НЕ СА NULLOPTION (MERGE JOIN); 

Формата на плана е такава, каквато се надявахме:

Поръчано сканиране на двата филтрирани индекса, страхотни оценки за мощността, фантастично. Само един малък проблем:този план за изпълнение е много по-лош; прогнозната цена е скочила от 0,0772056 на 0,741527 . Причината за скока в прогнозните разходи се разкрива чрез проверка на свойствата на оператора за свързване на сливане:

Това е скъпо присъединяване много към много, при което машината за изпълнение трябва да следи дубликатите от външния вход в работна таблица и да пренавива, ако е необходимо. Дубликати? Сканираме уникален индекс! Оказва се, че оптимизаторът не знае, че филтриран уникален индекс произвежда уникални стойности (свържете елемента тук). Всъщност това е едно към едно присъединяване, но оптимизаторът го струва, сякаш е много към много, обяснявайки защо предпочита плана за хеш присъединяване.

Алтернативна стратегия

Изглежда, че продължаваме да се сблъскваме с ограничения на оптимизатора, когато използваме филтрирани индекси тук (въпреки че това е подчертан случай на употреба в Books Online). Какво ще стане, ако вместо това се опитаме да използваме изгледи?

Използване на изгледи

Следните два изгледа просто филтрират базовите таблици, за да покажат редовете, където колоната с данни е NOT NULL :

СЪЗДАВАНЕ НА ИЗГЛЕД dbo.VA С SCEMABINDING ASSELECT pk, data, paddingFROM dbo.TableAWHERE данните НЕ СА NULL;GOCREATE VIEW dbo.VBWITH SCHEMABINDING ASSELECT pk, data, paddingFROM dbo.TableBWHERE данните НЕ СА NULL;

Пренаписването на оригиналната заявка за използване на изгледите е тривиално:

ИЗБЕРЕТЕ v.data, v2.dataFROM dbo.VA КАТО vJOIN dbo.VB AS v2 ON v.data =v2.data;

Не забравяйте, че тази заявка първоначално създаде план с паралелни вложени цикли на цена 7,7768 единици. С препратките за изглед получаваме този план за изпълнение:

Това е точно същия план за хеш присъединяване, който трябваше да добавим излишен NOT NULL предикати за получаване с филтрираните индекси (цената е 0,0772056 единици както преди). Това се очаква, защото всичко, което по същество направихме тук, е да натиснем допълнителния NOT NULL предикати от заявката към изглед.

Индексиране на изгледите

Можем също да опитаме да материализираме изгледите, като създадем уникален клъстериран индекс в колоната pk:

СЪЗДАВАНЕ НА УНИКАЛЕН КЛУСТРИРАН ИНДЕКС cuq НА dbo.VA (pk);СЪЗДАВАНЕ НА УНИКАЛЕН КЛУСТРИРАН ИНДЕКС cuq НА dbo.VB (pk);

Сега можем да добавяме уникални неклъстерирани индекси към филтрираната колона с данни в индексирания изглед:

СЪЗДАВАНЕ НА УНИКАЛЕН НЕКЛУСТРИРАН ИНДЕКС ix НА dbo.VA (данни); СЪЗДАВАНЕ НА УНИКАЛЕН НЕКЛУСТРИРАН ИНДЕКС ix НА dbo.VB (данни);

Забележете, че филтрирането се извършва в изгледа, тези неклъстерирани индекси сами по себе си не се филтрират.

Перфектният план

Вече сме готови да изпълним нашата заявка срещу изгледа, използвайки NOEXPAND съвет за таблица:

ИЗБЕРЕТЕ v.data, v2.dataFROM dbo.VA AS v С (NOEXPAND)JOIN dbo.VB AS v2 С (NOEXPAND) ON v.data =v2.data;

Планът за изпълнение е:

Оптимизаторът може да види нефилтрираното неклъстерираните индекси на изглед са уникални, така че не е необходимо обединяване много към много. Този окончателен план за изпълнение има приблизителна цена от 0,0310929 единици – дори по-ниско от плана за хеш присъединяване (0,0772056 единици). Това потвърждава очакванията ни, че обединяването за сливане трябва да има най-ниската прогнозна цена за тази заявка и примерен набор от данни.

NOEXPAND са необходими съвети дори в Enterprise Edition, за да се гарантира, че гаранцията за уникалност, предоставена от индексите на изгледите, се използва от оптимизатора.

Резюме

Тази публикация подчертава две важни ограничения на оптимизатора с филтрирани индекси:

  • Може да са необходими излишни предикати за присъединяване, за да съответстват на филтрирани индекси
  • Филтрираните уникални индекси не предоставят информация за уникалността на оптимизатора

В някои случаи може да е практично просто да добавите излишните предикати към всяка заявка. Алтернативата е да се капсулират желаните подразбиращи се предикати в неиндексиран изглед. Планът за хеширане в тази публикация беше много по-добър от плана по подразбиране, въпреки че оптимизаторът би трябвало да може да намери малко по-добрия план за присъединяване при сливане. Понякога може да се наложи да индексирате изгледа и да използвате NOEXPAND съвети (все пак се изискват за екземпляри от Standard Edition). При други обстоятелства нито един от тези подходи няма да е подходящ. Съжалявам за това :)


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Активиране на двуфакторна автентификация за ScaleGrid DBaaS

  2. Рамка на Apache Spark Job Run!

  3. Анализиране на ODBC данни в IBM SPSS

  4. ACID свойства на изявления и транзакции

  5. Как да възстановите база данни с Backup Manager