Огромное замедление запроса SQL Server при добавлении подстановочного знака (или верхней части)

52

У меня есть зоопарк из 20 миллионов животных, которые я отслеживаю в своей базе данных SQL Server 2005. Приблизительно 1% из них - черные, и приблизительно 1% из них - лебеди. Я хотел получить подробную информацию обо всех черных лебедях и поэтому, не желая забивать страницу результатов, которую я сделал:

select top 10 * 
from animal 
where colour like 'black'  
and species like 'swan'

(Да, непреднамеренно эти поля являются свободным текстом, но они оба проиндексированы). Оказывается, у нас нет таких животных, так как запрос возвращает пустой набор примерно за 300 миллисекунд. Это было бы примерно в два раза быстрее, если бы я использовал «=», а не «как», но у меня есть предчувствие, что последнее поможет мне немного набрать текст.

Оказывается, главный зоопарк думает, что он, возможно, ввел некоторых из лебедей как «черноватые», поэтому я соответствующим образом модифицирую запрос:

select top 10 * 
from animal  
where colour like 'black%' 
and species like 'swan'

Оказывается, что их тоже нет (и на самом деле нет «черных%» животных, кроме «черных»), но запрос теперь занимает около 30 секунд, чтобы вернуться пустым.

Кажется, что это только сочетание «top» и «like%», вызывающее проблемы, потому что

select count(*) 
from animal  
where colour like 'black%' 
and species like 'swan'

возвращает 0 очень быстро, и даже

select * 
from animal 
where colour like 'black%' 
and species like 'swan'

возвращается пустым в доли секунды.

Есть ли у кого-нибудь идеи, почему «top» и «%» должны сговориться, чтобы вызвать такую ​​резкую потерю производительности, особенно в пустом наборе результатов?

РЕДАКТИРОВАТЬ: просто чтобы уточнить, я не использую никаких индексов FreeText, я просто имел в виду, что поля являются свободным текстом в точке входа, то есть не нормализованы в базе данных. Извините за путаницу, плохая формулировка с моей стороны.

stovroz
источник

Ответы:

76

Это решение оптимизатора на основе затрат.

Оценочные затраты, использованные в этом выборе, неверны, так как они предполагают статистическую независимость между значениями в разных столбцах.

Это похоже на проблему, описанную в 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%'                         |
+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+
Мартин Смит
источник
15

Основываясь на этом интригующем, я провел некоторые поиски и наткнулся на этот вопрос. Как (и почему) TOP влияет на план выполнения?

По сути, использование TOP изменяет стоимость операторов, следующих за ним (нетривиальным образом), что также приводит к изменению общего плана (было бы здорово, если бы вы включили ExecPlans с и без TOP 10), что в значительной степени меняет общее выполнение запрос.

Надеюсь это поможет.

Например, я попробовал это на базе данных и: - когда не вызывается вершина, используется параллелизм - с вершиной, параллелизм не используется

Итак, опять же, показ ваших планов выполнения предоставит больше информации.

Хорошего дня

Владислав Залесак
источник
-1

Я полагаю, что это может быть связано с основной природой MSSQL 2005 и тем, как оптимизатор запросов решает, какой план выполнения является наиболее эффективным.

Если вы используете переменную SQL, он должен «обмануть» оптимизатор запросов, чтобы использовать хеш-совпадения вместо вложенных циклов, что приведет к гораздо более высокой степени параллелизма.

Пытаться:

DECLARE @topn INT = 10
SELECT TOP (@topn) *
FROM    animal
WHERE   colour LIKE 'black%' 
AND species LIKE 'swan'

источник
5
Запутывание TOPзначения в переменной означает, что оно будет принимать, TOP 100а не TOP 10. Это может или не может помочь в зависимости от того, что является переломным моментом между двумя планами.
Мартин Смит,