Підсумок
Основні проблеми:
- Вибір плану оптимізатора передбачає рівномірний розподіл значень.
- Відсутність відповідних індексів означає:
- Сканування таблиці - єдиний варіант.
- З'єднання - це наївне вкладене цикл приєднання, а не індекс вкладених циклів. При наївному з'єднанні предикати приєднання оцінюються при з'єднанні, а не відсуваються до внутрішньої сторони з'єднання.
Деталі
Два плани принципово дуже схожі, хоча продуктивність може бути дуже різною:
Плануйте за допомогою додаткових стовпців
Візьміть один із додаткових стовпців, який не завершується в розумний час:
Цікаві особливості:
- Вершина у вузлі 0 обмежує рядки, повернуті до 100. Також він встановлює мету рядка для оптимізатора, тому все, що знаходиться під ним у плані, вибирається для швидкого повернення перших 100 рядків.
- Сканування у вузлі 4 знаходить рядки з таблиці, де значення
Start_Time
не є нульовим, State
- 3 або 4 таOperation_Type
є одним із перелічених значень. Таблиця повністю сканується один раз, при цьому кожен рядок тестується на вказані предикати. На Сортування надходять лише рядки, які проходять усі тести. Оптимізатор підраховує, що 38 283 рядків будуть кваліфіковані.
- Сортування в вузлі 3 споживає всі рядки з "Сканування" у вузлі 4 і сортує їх у порядку
Start_Time DESC
. Це остаточне замовлення на презентацію, яке вимагає запит.
- Оптимізатор підраховує, що 93 рядків (фактично 93.2791) потрібно буде прочитати з сортування, щоб весь план повернув 100 рядків (враховуючи очікуваний ефект приєднання).
- Очікується, що з'єднання вкладених циклів у вузлі 2 виконає свій внутрішній вхід (нижня гілка) 94 рази (фактично 94,2791). Додатковий рядок необхідний обміном зупинки паралелізму у вузлі 1 з технічних причин.
- Сканування у вузлі 5 повністю сканує таблицю за кожною ітерацією. Він знаходить рядки, де
Start_Time
це не нульове значення, і State
це 3 або 4. За оцінками, виходить 400 875 рядків на кожній ітерації. Понад 94,2791 повторень загальна кількість рядків становить майже 38 мільйонів.
- Об'єднання вкладених циклів у вузлі 2 також застосовує предикати об'єднання. Він перевіряє
Operation_Type
відповідність, що Start_Time
від вузла 4 менше, ніж Start_Time
від вузла 5, що Start_Time
від вузла 5 менше, ніж Finish_Time
від вузла 4, і що два Id
значення не відповідають.
- Збір потоків (зупинка обміну паралелізмом) у вузлі 1 об'єднує впорядковані потоки з кожної нитки, поки не буде створено 100 рядків. Характер злиття, що зберігає порядок, у кількох потоках - це те, що вимагає додаткового рядка, згаданого на кроці 5.
Велика неефективність очевидно на етапах 6 і 7 вище. Повністю сканувати таблицю у вузлі 5 для кожної ітерації є розумним лише тоді, коли це відбувається лише 94 рази, як прогнозує оптимізатор. ~ 38 мільйонів за ряд рядів порівнянь у вузлі 2 також є великою вартістю.
Принципово важливо, що ціль рядкових рядків 93/94 також є цілком ймовірною помилкою, оскільки це залежить від розподілу значень. Оптимізатор передбачає рівномірний розподіл за відсутності більш детальної інформації. Простіше кажучи, це означає, що якщо очікується, що 1% рядків у таблиці буде кваліфікованим, оптимізатор пояснює, що для того, щоб знайти 1 відповідний рядок, йому потрібно прочитати 100 рядків.
Якщо ви запустили цей запит до його завершення (що може зайняти дуже багато часу), ви, швидше за все, виявите, що набагато більше 93/94 рядків потрібно було прочитати з сортування, щоб остаточно отримати 100 рядків. У гіршому випадку 100-й ряд буде знайдено за допомогою останнього рядка з сортування. Якщо припустити, що оцінка оптимізатора у вузлі 4 є правильною, це означає, що запустити Сканування у вузлі 5 38,284 рази, загалом приблизно 15 мільярдів рядків. Це може бути більше, якщо оцінки Сканування також вимкнено.
Цей план виконання також включає попередження про відсутність індексу:
/*
The Query Processor estimates that implementing the following index
could improve the query cost by 72.7096%.
WARNING: This is only an estimate, and the Query Processor is making
this recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide
impact, including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/
CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Batch_Tasks_Queue] ([Operation_Type],[State],[Start_Time])
INCLUDE ([Id],[Parameters])
Оптимізатор попереджає вас про те, що додавання індексу до таблиці покращить продуктивність.
Плануйте без зайвих стовпців
Це по суті такий самий план, що і попередній, з додаванням індексної котушки у вузлі 6 та фільтра у вузлі 5. Важливими відмінностями є:
- Індексна котушка у вузлі 6 - це Eager Spool. Він нетерпляче споживає результат сканування під ним, і будує тимчасовий індекс, набраний на,
Operation_Type
і Start_Time
, Id
як стовпець без ключа.
- Приєднання до вкладених циклів у вузлі 2 тепер є приєднанням до індексу. Немає предикати оцінюються тут, замість цього за ітерацію поточних значень
Operation_Type
, Start_Time
, Finish_Time
і Id
з перевірки на вузлі 4 передаються на внутрішній стороні гілка , як зовнішні посилання.
- Сканування у вузлі 7 виконується лише один раз.
- Індексна котушка у вузлі 6 шукає рядки з тимчасового індексу, де
Operation_Type
відповідає поточному зовнішньому опорному значенню, а значення Start_Time
знаходиться в діапазоні, визначеному Start_Time
та Finish_Time
зовнішніми посиланнями.
- Фільтр у вузлі 5 перевіряє
Id
значення з індексу котушки на нерівність проти поточного зовнішнього опорного значення Id
.
Ключові вдосконалення:
- Сканування з внутрішньої сторони проводиться лише один раз
- Тимчасовий індекс на (
Operation_Type
, Start_Time
) з Id
включеним стовпцем дозволяє індексу вкладених циклів з'єднати. Індекс використовується для пошуку відповідних рядків на кожній ітерації, а не для сканування всієї таблиці кожен раз.
Як і раніше, оптимізатор включає попередження про відсутність індексу:
/*
The Query Processor estimates that implementing the following index
could improve the query cost by 24.1475%.
WARNING: This is only an estimate, and the Query Processor is making
this recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide
impact, including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/
CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Batch_Tasks_Queue] ([State],[Start_Time])
INCLUDE ([Id],[Operation_Type])
GO
Висновок
План без зайвих стовпців швидший, оскільки оптимізатор вирішив створити для вас тимчасовий індекс.
План із додатковими стовпцями зробить тимчасовий індекс дорожчим для побудови. [Parameters
Колонка] є nvarchar(2000)
, який хотів би додати до 4000 байт для кожного рядка індексу. Додаткових витрат достатньо, щоб переконати оптимізатора, що побудова тимчасового індексу на кожне виконання не окупиться.
Оптимізатор попереджає в обох випадках, що постійний індекс був би кращим рішенням. Ідеальний склад індексу залежить від вашої ширшої навантаження. Для цього конкретного запиту запропоновані індекси є розумною відправною точкою, але ви повинні розуміти вигоди та витрати.
Рекомендація
Широкий спектр можливих індексів буде корисним для цього запиту. Важливим фактором є те, що потрібен якийсь некластеризований індекс. З наданої інформації, розумним індексом, на мою думку, було б:
CREATE NONCLUSTERED INDEX i1
ON dbo.Batch_Tasks_Queue (Start_Time DESC)
INCLUDE (Operation_Type, [State], Finish_Time);
Я також спокусився би трохи краще організувати запит і затримати пошук широких [Parameters]
стовпців у кластерному індексі до того моменту, поки не будуть знайдені топ-100 рядків (використовуючи Id
як ключ):
SELECT TOP (100)
BTQ1.id,
BTQ2.id,
BTQ3.[Parameters],
BTQ4.[Parameters]
FROM dbo.Batch_Tasks_Queue AS BTQ1
JOIN dbo.Batch_Tasks_Queue AS BTQ2 WITH (FORCESEEK)
ON BTQ2.Operation_Type = BTQ1.Operation_Type
AND BTQ2.Start_Time > BTQ1.Start_Time
AND BTQ2.Start_Time < BTQ1.Finish_Time
AND BTQ2.id != BTQ1.id
-- Look up the [Parameters] values
JOIN dbo.Batch_Tasks_Queue AS BTQ3
ON BTQ3.Id = BTQ1.Id
JOIN dbo.Batch_Tasks_Queue AS BTQ4
ON BTQ4.Id = BTQ2.Id
WHERE
BTQ1.[State] IN (3, 4)
AND BTQ2.[State] IN (3, 4)
AND BTQ1.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
AND BTQ2.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
-- These predicates are not strictly needed
AND BTQ1.Start_Time IS NOT NULL
AND BTQ2.Start_Time IS NOT NULL
ORDER BY
BTQ1.Start_Time DESC;
Якщо [Parameters]
стовпці не потрібні, запит можна спростити до:
SELECT TOP (100)
BTQ1.id,
BTQ2.id
FROM dbo.Batch_Tasks_Queue AS BTQ1
JOIN dbo.Batch_Tasks_Queue AS BTQ2 WITH (FORCESEEK)
ON BTQ2.Operation_Type = BTQ1.Operation_Type
AND BTQ2.Start_Time > BTQ1.Start_Time
AND BTQ2.Start_Time < BTQ1.Finish_Time
AND BTQ2.id != BTQ1.id
WHERE
BTQ1.[State] IN (3, 4)
AND BTQ2.[State] IN (3, 4)
AND BTQ1.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
AND BTQ2.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
AND BTQ1.Start_Time IS NOT NULL
AND BTQ2.Start_Time IS NOT NULL
ORDER BY
BTQ1.Start_Time DESC;
FORCESEEK
Підказка , щоб допомогти гарантувати , що оптимізатор вибирає індексовані вкладені цикли планування (є вартісний спокуса для оптимізатора , щоб вибрати хеш або (багато-багато) злиттям в іншому випадку, який , як правило , не хорошо працювати з цим типом запит на практиці. Обидва закінчуються великими залишками; багато елементів на відро у випадку хешу, і багато перемотування назад для злиття).
Альтернатива
Якщо запит (включаючи його конкретні значення) був особливо критичним для продуктивності читання, я б розглядав два відфільтровані індекси:
CREATE NONCLUSTERED INDEX i1
ON dbo.Batch_Tasks_Queue (Start_Time DESC)
INCLUDE (Operation_Type, [State], Finish_Time)
WHERE
Start_Time IS NOT NULL
AND [State] IN (3, 4)
AND Operation_Type <> 23
AND Operation_Type <> 24
AND Operation_Type <> 25
AND Operation_Type <> 26
AND Operation_Type <> 27
AND Operation_Type <> 28
AND Operation_Type <> 30;
CREATE NONCLUSTERED INDEX i2
ON dbo.Batch_Tasks_Queue (Operation_Type, [State], Start_Time)
WHERE
Start_Time IS NOT NULL
AND [State] IN (3, 4)
AND Operation_Type <> 23
AND Operation_Type <> 24
AND Operation_Type <> 25
AND Operation_Type <> 26
AND Operation_Type <> 27
AND Operation_Type <> 28
AND Operation_Type <> 30;
Для запиту, який не потребує [Parameters]
стовпця, орієнтовний план із використанням відфільтрованих індексів:
Сканування індексу автоматично повертає всі кваліфіковані рядки, не оцінюючи додаткових предикатів. Для кожної ітерації з'єднання вкладених циклів індекс шукає дві операції пошуку:
- Шукати префікс збігається на
Operation_Type
і State
= 3, потім шукати діапазон Start_Time
значень, залишковий предикат на Id
нерівність.
- Шукати префікс збігається на
Operation_Type
і State
= 4, потім шукати діапазон Start_Time
значень, залишковий предикат на Id
нерівність.
Там, де [Parameters]
потрібен стовпець, план запиту просто додає максимум 100 однотонних пошукових запитів для кожної таблиці:
На завершення слід розглянути можливість використання вбудованих стандартних цілих чисел, а не numeric
де це можливо.