Почему 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 сканирует первый индекс, что приводит к 189 703 логическим чтениям и продолжительности 2-3 секунды.

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

Мне нужна переменная, но я хочу, чтобы SQL использовал хороший план. В качестве временного решения я поставил подсказку по индексу на запрос, и запрос в основном мгновенный. Тем не менее, я стараюсь держаться подальше от указателей, когда это возможно. Я обычно предполагаю, что если оптимизатор запросов не может выполнить свою работу, то я могу кое-что сделать (или прекратить делать), чтобы помочь ему без явного указания, что делать.

Итак, почему SQL Server предлагает лучший план, когда я включаю переменную?

Rainbolt
источник

Ответы:

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;

План будет выглядеть так:

NUTS

И примерное количество строк для этой локальной переменной будет выглядеть так:

NUTS

Несмотря на то, что запрос возвращает число 4 744 427.

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

NUTS

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 M строк в таблицу, и половина из них имеет значение '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

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

Джон на все руки
источник