Por que o SQL Server usa um plano de execução melhor quando inline a variável?

32

Eu tenho uma consulta SQL que estou tentando otimizar:

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 possui dois índices:

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)

Quando executo a consulta exatamente como descrito acima, o SQL Server verifica o primeiro índice, resultando em 189.703 leituras lógicas e uma duração de 2-3 segundos.

Quando inline a @Idvariável e executo a consulta novamente, o SQL Server procura o segundo índice, resultando em apenas 104 leituras lógicas e uma duração de 0,001 segundo (basicamente instantânea).

Preciso da variável, mas quero que o SQL use o bom plano. Como solução temporária, coloquei uma dica de índice na consulta, e a consulta é basicamente instantânea. No entanto, tento ficar longe das dicas de índice, quando possível. Normalmente, presumo que, se o otimizador de consultas não puder executar seu trabalho, há algo que eu posso fazer (ou parar de fazer) para ajudá-lo sem explicitamente dizer o que fazer.

Então, por que o SQL Server cria um plano melhor quando eu inline a variável?

Rainbolt
fonte

Respostas:

44

No SQL Server, existem três formas comuns de predicado de não associação:

Com um valor literal :

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

Com um parâmetro :

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

Com uma variável local :

DECLARE @Reputation INT = 1

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

Resultados

Quando você usa um valor literal e seu plano não é a) Trivial eb) Parametrizado Simples ou c) você não tem a Parametrização Forçada ativada, o otimizador cria um plano muito especial apenas para esse valor.

Quando você usa um parâmetro , o otimizador cria um plano para esse parâmetro (isso é chamado de sniffing de parâmetro ) e, em seguida, reutiliza esse plano, ausências de recompilação ausentes, planejamento de remoção de cache etc.

Quando você usa uma variável local , o otimizador faz um plano para ... Algo .

Se você executasse esta consulta:

DECLARE @Reputation INT = 1

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

O plano ficaria assim:

NUTS

E o número estimado de linhas para essa variável local ficaria assim:

NUTS

Mesmo que a consulta retorne uma contagem de 4.744.427.

As variáveis ​​locais, sendo desconhecidas, não usam a parte 'boa' do histograma para estimativa de cardinalidade. Eles usam um palpite baseado no vetor de densidade.

NUTS

SELECT 5.280389E-05 * 7250739 AS [poo]

Isso dará a você 382.86722457471, que é o palpite que o otimizador faz.

Essas suposições desconhecidas geralmente são muito ruins e geralmente podem levar a planos ruins e escolhas ruins de índice.

Consertando-o?

Suas opções geralmente são:

  • Dicas de índice frágeis
  • Sugestões de recompilação potencialmente caras
  • SQL dinâmico parametrizado
  • Um procedimento armazenado
  • Melhore o índice atual

Suas opções são especificamente:

Melhorar o índice atual significa estendê-lo para cobrir todas as colunas necessárias à consulta:

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

Supondo que os Idvalores sejam razoavelmente seletivos, isso fornecerá um bom plano e ajudará o otimizador, fornecendo a ele um método de acesso a dados 'óbvio'.

Mais leitura

Você pode ler mais sobre a incorporação de parâmetros aqui:

Erik Darling
fonte
12

Suponho que você tenha inclinado dados, que não deseja usar dicas de consulta para forçar o otimizador a fazer o que deve ser feito e que seja necessário obter um bom desempenho para todos os valores possíveis de entrada de @Id. Você pode obter um plano de consulta garantido para exigir apenas algumas poucas leituras lógicas para qualquer valor de entrada possível, se estiver disposto a criar o seguinte par de índices (ou seu equivalente):

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

Abaixo estão os meus dados de teste. Coloquei 13 M linhas na tabela e fiz com que metade delas tivesse um valor '3A35EA17-CE7E-4637-8319-4C517B6E48CA'para a Idcoluna.

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;

Esta consulta pode parecer um pouco estranha no começo:

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;

Ele foi desenvolvido para aproveitar a ordem dos índices para encontrar o valor mínimo ou máximo com algumas leituras lógicas. O CROSS JOINque há para obter resultados corretos quando não existem quaisquer linhas correspondentes para o @Idvalor. Mesmo se eu filtrar o valor mais popular da tabela (correspondendo a 6,5 ​​milhões de linhas), recebo apenas 8 leituras lógicas:

Tabela 'MinhaTabela'. Contagem de digitalizações 2, leituras lógicas 8

Aqui está o plano de consulta:

insira a descrição da imagem aqui

Ambos os índices procuram encontrar 0 ou 1 linhas. É extremamente eficiente, mas a criação de dois índices pode ser um exagero para o seu cenário. Você pode considerar o seguinte índice:

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

Agora, o plano de consulta da consulta original (com uma MAXDOP 1dica opcional ) parece um pouco diferente:

insira a descrição da imagem aqui

As principais pesquisas não são mais necessárias. Com um caminho de acesso melhor que funcione bem para todas as entradas, você não precisa se preocupar com o otimizador que escolhe o plano de consulta errado devido ao vetor de densidade. No entanto, essa consulta e esse índice não serão tão eficientes quanto o outro se você buscar um @Idvalor popular .

Tabela 'MinhaTabela'. Contagem de varreduras 1, leituras lógicas 33757

Joe Obbish
fonte
2

Não sei responder por que aqui, mas a maneira rápida e suja de garantir que a consulta seja executada da maneira que você deseja:

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

Isso implica o risco de a tabela ou os índices serem alterados no futuro, de modo que essa otimização se torne disfuncional, mas estará disponível se você precisar. Espero que alguém possa oferecer uma resposta de causa raiz, conforme solicitado, em vez desta solução alternativa.

Jon de todos os comércios
fonte