Это решение оптимизатора на основе затрат.
Оценочные затраты, использованные в этом выборе, неверны, так как они предполагают статистическую независимость между значениями в разных столбцах.
Это похоже на проблему, описанную в Row Goal 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
поэтому этот план возможен.
Операторы плана запроса выполняются слева направо . Оператор left запрашивает строки у своих дочерних элементов, которые, в свою очередь, запрашивают строки у своих дочерних элементов (и так далее, пока не будут достигнуты конечные узлы). 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
сканирование нужно обработать более 100000 строк.
Это связано с «предположением о включении», когда 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
. Это может или не может помочь в зависимости от того, что является переломным моментом между двумя планами.