Величезне уповільнення до запиту 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 секунд, щоб повернутися порожньою.

Здається, це лише комбінація "верхнього" та "як%", що викликає проблеми

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

повертає 0 дуже швидко, і навіть

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

повертається порожнім за частку секунди.

Хтось має ідею, чому «топ» і «%» повинні змовлятися, щоб спричинити таку різку втрату продуктивності, особливо в порожньому наборі результатів?

EDIT: Просто для уточнення, я не використовую жодних індексів FreeText, я просто мав на увазі, що поля є вільним текстом у точці введення, тобто не нормалізуються в базі даних. Вибачте за плутанину, погану формулювання з мого боку.

Відповіді:


76

Це рішення оптимізатора на основі витрат.

Приблизні витрати, використані в цьому виборі, є невірними, оскільки передбачають статистичну незалежність між значеннями в різних стовпцях.

Це схоже на проблему, описану в " 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%'                         |
+----------------------------------------------+-------------------+----------------------------------------------------------------+----------------------------------------------+

15

Виявивши цю інтригуючу інформацію, я здійснив пошук і натрапив на це питання / як (і чому) ТОП впливає на план виконання?

В основному, використання 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. Це може чи не допоможе залежно від того, яка переломна точка між двома планами.
Мартін Сміт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.