Запит не відповідає при додаванні двох стовпців


9

Коли я додаю два стовпці до мого вибору, запит не відповідає. Тип стовпця - це nvarchar(2000). Це трохи незвично.

  • Версія SQL Server - 2014 рік.
  • Є лише один первинний індекс.
  • Цілі записи - лише 1000 рядків.

Ось план виконання ( демонстраційний план XML ):

введіть тут опис зображення

План виконання після ( XML showplan ):

введіть тут опис зображення

Ось запит:

select top(100)
  Batch_Tasks_Queue.id,
  btq.id,
  Batch_Tasks_Queue.[Parameters], -- this field
  btq.[Parameters]  -- and this field
from
        Batch_Tasks_Queue with(nolock)
    inner join  Batch_Tasks_Queue btq with(nolock)  on  Batch_Tasks_Queue.Start_Time < btq.Start_Time
                            and btq.Start_Time < Batch_Tasks_Queue.Finish_Time
                            and Batch_Tasks_Queue.id <> btq.id                            
                            and btq.Start_Time is not null
                            and btq.State in (3, 4)                          
where
    Batch_Tasks_Queue.Start_Time is not null      
    and Batch_Tasks_Queue.State in (3, 4)
    and Batch_Tasks_Queue.Operation_Type = btq.Operation_Type
    and Batch_Tasks_Queue.Operation_Type not in (23, 24, 25, 26, 27, 28, 30)

order by
    Batch_Tasks_Queue.Start_Time desc

Загальний підрахунок результатів - 17 рядів. Брудні дані (натяк на блокування) не важливі.

Ось структура таблиці:

CREATE TABLE [dbo].[Batch_Tasks_Queue](
    [Id] [int] NOT NULL,
    [OBJ_VERSION] [numeric](8, 0) NOT NULL,
    [Operation_Type] [numeric](2, 0) NULL,
    [Request_Time] [datetime] NOT NULL,
    [Description] [varchar](1000) NULL,
    [State] [numeric](1, 0) NOT NULL,
    [Start_Time] [datetime] NULL,
    [Finish_Time] [datetime] NULL,
    [Parameters] [nvarchar](2000) NULL,
    [Response] [nvarchar](max) NULL,
    [Billing_UserId] [int] NOT NULL,
    [Planned_Start_Time] [datetime] NULL,
    [Input_FileId] [uniqueidentifier] NULL,
    [Output_FileId] [uniqueidentifier] NULL,
    [PRIORITY] [numeric](2, 0) NULL,
    [EXECUTE_SEQ] [numeric](2, 0) NULL,
    [View_Access] [numeric](1, 0) NULL,
    [Seeing] [numeric](1, 0) NULL,
 CONSTRAINT [PKBachTskQ] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [Batch_Tasks_QueueData]
) ON [Batch_Tasks_QueueData] TEXTIMAGE_ON [Batch_Tasks_QueueData]
GO    
SET ANSI_PADDING OFF
GO
ALTER TABLE [dbo].[Batch_Tasks_Queue]  WITH NOCHECK ADD  CONSTRAINT [FK0_BtchTskQ_BlngUsr] FOREIGN KEY([Billing_UserId])
REFERENCES [dbo].[BILLING_USER] ([ID])
GO
ALTER TABLE [dbo].[Batch_Tasks_Queue] CHECK CONSTRAINT [FK0_BtchTskQ_BlngUsr]
GO

Обговорення цього питання було переміщено до цієї кімнати чату .
Пол Білий 9

Відповіді:


15

Підсумок

Основні проблеми:

  • Вибір плану оптимізатора передбачає рівномірний розподіл значень.
  • Відсутність відповідних індексів означає:
    • Сканування таблиці - єдиний варіант.
    • З'єднання - це наївне вкладене цикл приєднання, а не індекс вкладених циклів. При наївному з'єднанні предикати приєднання оцінюються при з'єднанні, а не відсуваються до внутрішньої сторони з'єднання.

Деталі

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

Плануйте за допомогою додаткових стовпців

Візьміть один із додаткових стовпців, який не завершується в розумний час:

Повільний план

Цікаві особливості:

  1. Вершина у вузлі 0 обмежує рядки, повернуті до 100. Також він встановлює мету рядка для оптимізатора, тому все, що знаходиться під ним у плані, вибирається для швидкого повернення перших 100 рядків.
  2. Сканування у вузлі 4 знаходить рядки з таблиці, де значення Start_Timeне є нульовим, State- 3 або 4 таOperation_Type є одним із перелічених значень. Таблиця повністю сканується один раз, при цьому кожен рядок тестується на вказані предикати. На Сортування надходять лише рядки, які проходять усі тести. Оптимізатор підраховує, що 38 283 рядків будуть кваліфіковані.
  3. Сортування в вузлі 3 споживає всі рядки з "Сканування" у вузлі 4 і сортує їх у порядку Start_Time DESC. Це остаточне замовлення на презентацію, яке вимагає запит.
  4. Оптимізатор підраховує, що 93 рядків (фактично 93.2791) потрібно буде прочитати з сортування, щоб весь план повернув 100 рядків (враховуючи очікуваний ефект приєднання).
  5. Очікується, що з'єднання вкладених циклів у вузлі 2 виконає свій внутрішній вхід (нижня гілка) 94 рази (фактично 94,2791). Додатковий рядок необхідний обміном зупинки паралелізму у вузлі 1 з технічних причин.
  6. Сканування у вузлі 5 повністю сканує таблицю за кожною ітерацією. Він знаходить рядки, де Start_Timeце не нульове значення, і Stateце 3 або 4. За оцінками, виходить 400 875 рядків на кожній ітерації. Понад 94,2791 повторень загальна кількість рядків становить майже 38 мільйонів.
  7. Об'єднання вкладених циклів у вузлі 2 також застосовує предикати об'єднання. Він перевіряє Operation_Typeвідповідність, що Start_Timeвід вузла 4 менше, ніж Start_Timeвід вузла 5, що Start_Timeвід вузла 5 менше, ніж Finish_Timeвід вузла 4, і що два Idзначення не відповідають.
  8. Збір потоків (зупинка обміну паралелізмом) у вузлі 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. Важливими відмінностями є:

  1. Індексна котушка у вузлі 6 - це Eager Spool. Він нетерпляче споживає результат сканування під ним, і будує тимчасовий індекс, набраний на, Operation_Typeі Start_Time, Idяк стовпець без ключа.
  2. Приєднання до вкладених циклів у вузлі 2 тепер є приєднанням до індексу. Немає предикати оцінюються тут, замість цього за ітерацію поточних значень Operation_Type, Start_Time, Finish_Timeі Idз перевірки на вузлі 4 передаються на внутрішній стороні гілка , як зовнішні посилання.
  3. Сканування у вузлі 7 виконується лише один раз.
  4. Індексна котушка у вузлі 6 шукає рядки з тимчасового індексу, де Operation_Typeвідповідає поточному зовнішньому опорному значенню, а значення Start_Timeзнаходиться в діапазоні, визначеному Start_Timeта Finish_Timeзовнішніми посиланнями.
  5. Фільтр у вузлі 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]стовпця, орієнтовний план із використанням відфільтрованих індексів:

Простий відфільтрований індексний план

Сканування індексу автоматично повертає всі кваліфіковані рядки, не оцінюючи додаткових предикатів. Для кожної ітерації з'єднання вкладених циклів індекс шукає дві операції пошуку:

  1. Шукати префікс збігається на Operation_Typeі State= 3, потім шукати діапазон Start_Timeзначень, залишковий предикат на Idнерівність.
  2. Шукати префікс збігається на Operation_Typeі State= 4, потім шукати діапазон Start_Timeзначень, залишковий предикат на Idнерівність.

Там, де [Parameters]потрібен стовпець, план запиту просто додає максимум 100 однотонних пошукових запитів для кожної таблиці:

Фільтрований індексний план із додатковими колонками

На завершення слід розглянути можливість використання вбудованих стандартних цілих чисел, а не numericде це можливо.


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.