Це рішення оптимізатора на основі витрат.
Приблизні витрати, використані в цьому виборі, є невірними, оскільки передбачають статистичну незалежність між значеннями в різних стовпцях.
Це схоже на проблему, описану в " Row Goals Gone Rogue", де парні і непарні числа негативно співвідносяться.
Відтворюється легко.
CREATE TABLE dbo.animal(
id int IDENTITY(1,1) NOT NULL PRIMARY KEY,
colour varchar(50) NOT NULL,
species varchar(50) NOT NULL,
Filler char(10) NULL
);
/*Insert 20 million rows with 1% black and 1% swan but no black swans*/
WITH T
AS (SELECT TOP 20000000 ROW_NUMBER() OVER (ORDER BY @@SPID) AS RN
FROM master..spt_values v1,
master..spt_values v2,
master..spt_values v3)
INSERT INTO dbo.animal
(colour,
species)
SELECT CASE
WHEN RN % 100 = 1 THEN 'black'
ELSE CAST(RN % 100 AS VARCHAR(3))
END,
CASE
WHEN RN % 100 = 2 THEN 'swan'
ELSE CAST(RN % 100 AS VARCHAR(3))
END
FROM T
/*Create some indexes*/
CREATE NONCLUSTERED INDEX ix_species ON dbo.animal(species);
CREATE NONCLUSTERED INDEX ix_colour ON dbo.animal(colour);
Тепер спробуйте
SELECT TOP 10 *
FROM animal
WHERE colour LIKE 'black'
AND species LIKE 'swan'
Це дає план, нижче якого коштують 0.0563167
.
План може виконати з'єднання злиття між результатами двох індексів id
стовпця. ( Детальніше про алгоритм приєднання об'єднатись тут ).
Об'єднання об'єднань вимагає, щоб обидва входи були упорядковані ключем приєднання.
Некластеризовані індекси впорядковані відповідно (species, id)
і (colour, id)
відповідно (ненукізовані некластеризовані індекси завжди містять рядок локатора рядків у кінці ключа неявно, якщо не додаються явно). Запит без будь-яких подкапових знаків виконує пошук рівності в species = 'swan'
і colour ='black'
. Оскільки кожне прагнення отримує лише одне точне значення з провідного стовпця, відповідні рядки будуть впорядковані, id
тому такий план можливий.
Оператори плану запитів виконують зліва направо . Якщо лівий оператор вимагає рядків від своїх дітей, які в свою чергу запитують рядки від своїх дітей (і так далі, поки не будуть досягнуті вузли листя). TOP
Ітератора зупиниться запитуючи кілька рядків зі своєї дитини , як тільки 10 були отримані.
SQL Server має статистичні дані щодо індексів, які свідчать про те, що 1% рядків відповідає кожному предикату. Він передбачає, що ця статистика є незалежною (тобто не співвідноситься ні позитивно, ні негативно), так що в середньому, коли вона обробила 1000 рядків, що відповідають першому предикату, вона знайде 10, що відповідають другому, і може вийти. (план вище насправді показує 987, а не 1000, але досить близький).
Насправді, коли предикати негативно співвідносяться, фактичний план показує, що всі 200 000 відповідних рядків потрібно обробити з кожного індексу, але це певною мірою пом'якшується, тому що нульові рядки також означають, що нульові пошуки дійсно потрібні.
Порівняйте з
SELECT TOP 10 *
FROM animal
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
Що дає план нижче, на який коштують 0.567943
Додавання останньої підстановки призвело до сканування індексу. Вартість плану все ще досить низька, хоча для сканування на 20 мільйонів рядків таблиці.
Додавання querytraceon 9130
показує додаткову інформацію
SELECT TOP 10 *
FROM animal
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
OPTION (QUERYTRACEON 9130)
Видно, що SQL Server вважає, що йому потрібно буде сканувати близько 100 000 рядків, перш ніж він знайде 10, що відповідають предикату, і TOP
може припинити запитувати рядки.
Знову це має сенс з припущенням незалежності як 10 * 100 * 100 = 100,000
Нарешті давайте спробуємо застосувати план перетину індексу
SELECT TOP 10 *
FROM animal WITH (INDEX(ix_species), INDEX(ix_colour))
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
Це дає паралельний план для мене із оціночною вартістю 3,4625
Основна відмінність тут полягає в тому, що colour like 'black%'
присудок тепер може збігатися з декількома різними кольорами. Це означає, що відповідні рядки індексу для цього предиката більше не гарантовано сортуються в порядку id
.
Наприклад, пошук за індексом like 'black%'
може повертати наступні рядки
+------------+----+
| Colour | id |
+------------+----+
| black | 12 |
| black | 20 |
| black | 23 |
| black | 25 |
| blackberry | 1 |
| blackberry | 50 |
+------------+----+
Ідентифікатори в кожному кольорі впорядковані, але ідентифікатори різних кольорів можуть бути відсутніми.
У результаті SQL Server більше не може виконувати перетин переліку індексу об'єднання (не додаючи оператора сортування блокування), і замість цього він вирішує виконувати хеш-з'єднання. Hash Join блокує введення збірки, тому тепер вартість відображає той факт, що всі відповідні рядки потрібно буде обробити з введення збірки, а не припускати, що доведеться сканувати лише 1000, як у першому плані.
Однак вхід зонда не блокує, і він все ще невірно оцінює, що він зможе зупинити зондування після обробки 987 рядків з цього.
(Докладніші відомості про неблокуючі проти блокування ітераторів тут)
Зважаючи на збільшення витрат на додаткові розрахункові рядки та приєднання хешу, часткове кластеризоване сканування індексу виглядає дешевше.
На практиці, звичайно, "часткове" кластерне сканування індексів зовсім не є частковим, і його потрібно переглядати через цілі 20 мільйонів рядків, а не 100 тисяч, що передбачаються при порівнянні планів.
Збільшення значення TOP
(або вилучення його повністю) зрештою стикається з переломною точкою, де кількість рядків, які він оцінює, потрібно буде покрити скануванням CI, і цей план виглядає дорожче, і він повертається до плану перетину індексу. Для мене точка відрізку між двома планами - TOP (89)
проти TOP (90)
.
Для вас це може відрізнятися, оскільки це залежить від того, наскільки широкий кластерний індекс.
Видалення TOP
та примушування сканування CI
SELECT *
FROM animal WITH (INDEX = 1)
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
Коштується 88.0586
на моїй машині для мого прикладу таблиці.
Якби SQL Server знав, що в зоопарку немає чорних лебедів і йому потрібно буде зробити повне сканування, а не просто прочитати 100 000 рядків, цей план не був би обраний.
Я спробував статистику з декількома стовпцями на animal(species,colour)
та animal(colour,species)
відфільтровану статистику, animal (colour) where species = 'swan'
але жодна з них не переконує, що чорних лебедів не існує, і для TOP 10
сканування потрібно буде обробити більше 100 000 рядків.
Це пов'язано з "припущенням включення", де SQL Server по суті припускає, що якщо ви шукаєте щось, можливо, воно існує.
На 2008 рік + є документально підтверджений прапор сліду 4138, який вимикає цілі рядків. Ефект цього полягає в тому, що план коштується без припущення, що TOP
дозволення дозволить дітям-операторам припинити достроково, не читаючи всіх відповідних рядків. Якщо цей прапор слід встановити, я, звичайно, отримую більш оптимальний план перетину індексу.
SELECT TOP 10 *
FROM animal
WHERE colour LIKE 'black%'
AND species LIKE 'swan'
OPTION (QUERYTRACEON 4138)
Цей план тепер правильно витрачає на читання повних 200 тис. Рядків в обох індексах, але перевищує вартість ключових пошукових запитів (орієнтовно 2 тис. Проти фактичних 0. Слід TOP 10
обмежити це максимум 10, але прапор трас заважає це враховувати) . Все-таки план коштує значно дешевше, ніж вибрано повне сканування CI.
Звичайно, цей план не може бути оптимальним для комбінацій, які є загальними. Такі як білі лебеді.
Складений індекс на animal (colour, species)
або в ідеалі animal (species, colour)
дозволив би зробити запит набагато ефективнішим для обох сценаріїв.
Щоб максимально ефективно використовувати складений індекс, його LIKE 'swan'
також потрібно змінити = 'swan'
.
У таблиці нижче показані предикати пошуку та залишкові предикати, показані в планах виконання всіх чотирьох перестановок.
+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
| WHERE clause | Index | Seek Predicate | Residual Predicate |
+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
| colour LIKE 'black%' AND species LIKE 'swan' | ix_colour_species | colour >= 'black' AND colour < 'blacL' | colour like 'black%' AND species like 'swan' |
| colour LIKE 'black%' AND species LIKE 'swan' | ix_species_colour | species >= 'swan' AND species <= 'swan' | colour like 'black%' AND species like 'swan' |
| colour LIKE 'black%' AND species = 'swan' | ix_colour_species | (colour,species) >= ('black', 'swan')) AND colour < 'blacL' | colour LIKE 'black%' AND species = 'swan' |
| colour LIKE 'black%' AND species = 'swan' | ix_species_colour | species = 'swan' AND (colour >= 'black' and colour < 'blacL') | colour like 'black%' |
+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
TOP
значення змінної означає, що воно буде вважати,TOP 100
а неTOP 10
. Це може чи не допоможе залежно від того, яка переломна точка між двома планами.