Чому SQL Server використовує кращий план виконання, коли я встроюю змінну?


32

У мене є SQL-запит, який я намагаюся оптимізувати:

DECLARE @Id UNIQUEIDENTIFIER = 'cec094e5-b312-4b13-997a-c91a8c662962'

SELECT 
  Id,
  MIN(SomeTimestamp),
  MAX(SomeInt)
FROM dbo.MyTable
WHERE Id = @Id
  AND SomeBit = 1
GROUP BY Id

MyTable має два індекси:

CREATE NONCLUSTERED INDEX IX_MyTable_SomeTimestamp_Includes
ON dbo.MyTable (SomeTimestamp ASC)
INCLUDE(Id, SomeInt)

CREATE NONCLUSTERED INDEX IX_MyTable_Id_SomeBit_Includes
ON dbo.MyTable (Id, SomeBit)
INCLUDE (TotallyUnrelatedTimestamp)

Коли я виконую запит точно так, як написано вище, SQL Server сканує перший індекс, в результаті чого ходять 189703 логічні зчитування та тривалість 2-3 секунди.

Коли я встроюю @Idзмінну і виконую запит ще раз, SQL Server шукає другий індекс, в результаті чого лише 104 логічні зчитування та тривалість 0,001 секунди (в основному миттєвий).

Мені потрібна змінна, але я хочу, щоб SQL використовував хороший план. Як тимчасове рішення я поставив індексний натяк на запит, і запит є в основному миттєвим. Однак я намагаюся уникати індексних підказів, коли це можливо. Зазвичай я припускаю, що якщо оптимізатор запитів не в змозі виконати свою роботу, я можу зробити (або перестати робити), щоб допомогти йому, не кажучи явно, що робити.

Отже, чому SQL Server придумує кращий план, коли я встроюю змінну?

Відповіді:


44

У SQL Server є три поширених форми предиката неприєднання:

З буквальним значенням:

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = 1;

З параметром :

CREATE PROCEDURE dbo.SomeProc(@Reputation INT)
AS
BEGIN
    SELECT COUNT(*) AS records
    FROM   dbo.Users AS u
    WHERE  u.Reputation = @Reputation;
END;

З локальною змінною :

DECLARE @Reputation INT = 1

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = @Reputation;

Результати

Якщо ви використовуєте буквальне значення, а ваш план не є а) Тривіальним і б) простим параметризованим або в) у вас не увімкнено примусову параметризацію , оптимізатор створює дуже спеціальний план саме для цього значення.

Коли ви використовуєте параметр , оптимізатор створить план для цього параметра (це називається нюхуванням параметра ), а потім повторно використовувати цей план, відсутні підказки щодо перекомпіляції, вилучення кешу плану тощо.

Коли ви використовуєте локальну змінну , оптимізатор складає план для ... Щось .

Якщо ви запустили цей запит:

DECLARE @Reputation INT = 1

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = @Reputation;

План виглядатиме так:

Горіхи

І орієнтовна кількість рядків для цієї локальної змінної виглядатиме так:

Горіхи

Незважаючи на те, що запит повертає кількість 4,744,427.

Локальні змінні, невідомі, не використовують «добру» частину гістограми для оцінки кардинальності. Вони використовують здогадку на основі вектора щільності.

Горіхи

SELECT 5.280389E-05 * 7250739 AS [poo]

Це дасть вам 382.86722457471, що є припущенням оптимізатора.

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

Виправити це?

Ваші варіанти, як правило:

  • Крихітний натяк наказів
  • Потенційно дорогі підказки щодо перекомпіляції
  • Параметризований динамічний SQL
  • Збережена процедура
  • Поліпшення поточного індексу

Ваші варіанти:

Поліпшення поточного індексу означає його розширення для охоплення всіх стовпців, необхідних для запиту:

CREATE NONCLUSTERED INDEX IX_MyTable_Id_SomeBit_Includes
ON dbo.MyTable (Id, SomeBit)
INCLUDE (TotallyUnrelatedTimestamp, SomeTimestamp, SomeInt)
WITH (DROP_EXISTING = ON);

Якщо припустити, що Idзначення досить вибіркові, це дасть вам хороший план та допоможе оптимізатору, надавши йому "очевидний" метод доступу до даних.

Більше читання

Докладніше про вбудовування параметрів можна прочитати тут:


12

Я припускаю, що ви перекосили дані, що ви не хочете використовувати підказки запитів, щоб змусити оптимізатора, що робити, і що вам потрібно отримати хороші показники для всіх можливих вхідних значень @Id. Ви можете отримати план запитів із гарантією, що вимагає лише декількох логічних зчитувань для будь-якого можливого вхідного значення, якщо ви готові створити наступну пару індексів (або їх еквівалент):

CREATE INDEX GetMinSomeTimestamp ON dbo.MyTable (Id, SomeTimestamp) WHERE SomeBit = 1;
CREATE INDEX GetMaxSomeInt ON dbo.MyTable (Id, SomeInt) WHERE SomeBit = 1;

Нижче наведені мої дані тесту. Я помістив 13 М рядків у таблицю, і половина з них має значення '3A35EA17-CE7E-4637-8319-4C517B6E48CA'для Idстовпця.

DROP TABLE IF EXISTS dbo.MyTable;

CREATE TABLE dbo.MyTable (
    Id uniqueidentifier,
    SomeTimestamp DATETIME2,
    SomeInt INT,
    SomeBit BIT,
    FILLER VARCHAR(100)
);

INSERT INTO dbo.MyTable WITH (TABLOCK)
SELECT NEWID(), CURRENT_TIMESTAMP, 0, 1, REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

INSERT INTO dbo.MyTable WITH (TABLOCK)
SELECT '3A35EA17-CE7E-4637-8319-4C517B6E48CA', CURRENT_TIMESTAMP, 0, 1, REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

Цей запит спочатку може виглядати трохи дивним:

DECLARE @Id UNIQUEIDENTIFIER = '3A35EA17-CE7E-4637-8319-4C517B6E48CA'

SELECT
  @Id,
  st.SomeTimestamp,
  si.SomeInt
FROM (
    SELECT TOP (1) SomeInt, Id
    FROM dbo.MyTable
    WHERE Id = @Id
    AND SomeBit = 1
    ORDER BY SomeInt DESC
) si
CROSS JOIN (
    SELECT TOP (1) SomeTimestamp, Id
    FROM dbo.MyTable
    WHERE Id = @Id
    AND SomeBit = 1
    ORDER BY SomeTimestamp ASC
) st;

Він створений для того, щоб скористатися впорядкованістю індексів для знаходження мінімального чи максимального значення з кількома логічними показаннями. Це CROSS JOINє для отримання правильних результатів, коли для цього @Idзначення немає відповідних рядків . Навіть якщо я фільтрую найпопулярніше значення таблиці (відповідає 6,5 мільйона рядків), я отримую лише 8 логічних зчитувань:

Таблиця "MyTable". Кількість сканувань 2, логічні зчитування 8

Ось план запитів:

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

Обидва показники шукають знаходження 0 або 1 рядків. Це надзвичайно ефективно, але створити два індекси може бути надмірним для вашого сценарію. Ви можете замість цього розглянути наступний індекс:

CREATE INDEX CoveringIndex ON dbo.MyTable (Id) INCLUDE (SomeTimestamp, SomeInt) WHERE SomeBit = 1;

Тепер план запиту оригінального запиту (з необов'язковим MAXDOP 1підказом) виглядає дещо інакше:

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

Пошук ключів більше не потрібний. З кращим доступом до доступу, який повинен працювати добре для всіх входів, вам не доведеться турбуватися про те, що оптимізатор вибирає неправильний план запитів через вектор густини. Однак цей запит та індекс не будуть настільки ефективними, як інші, якщо ви шукаєте популярне @Idзначення.

Таблиця "MyTable". Кількість сканувань 1, логічне зчитування 33757


2

Я не можу відповісти, чому тут, але швидкий і брудний спосіб забезпечити виконання запиту так, як вам потрібно:

DECLARE @Id UNIQUEIDENTIFIER = 'cec094e5-b312-4b13-997a-c91a8c662962'
SELECT 
  Id,
  MIN(SomeTimestamp),
  MAX(SomeInt)
FROM dbo.MyTable WITH (INDEX(IX_MyTable_Id_SomeBit_Includes))
WHERE Id = @Id
  AND SomeBit = 1
GROUP BY Id

Це створює ризик того, що таблиця або індекси можуть змінитися в майбутньому таким чином, що ця оптимізація стане нефункціональною, але вона доступна, якщо вона вам потрібна. Сподіваємось, хтось може запропонувати вам відповідь першопричини, як ви просили, а не це рішення.

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