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
tem 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 escrito acima, o SQL Server verifica o primeiro índice, resultando em 189.703 leituras lógicas e uma duração de 2 a 3 segundos.
Quando inline a @Id
variável e executo a consulta novamente, o SQL Server busca o segundo índice, resultando em apenas 104 leituras lógicas e uma duração de 0,001 segundo (basicamente instantânea).
Eu preciso da variável, mas quero que o SQL use o bom plano. Como solução temporária, coloco uma dica de índice na consulta, e a consulta é basicamente instantânea. No entanto, tento ficar longe de dicas de índice quando possível. Normalmente, presumo que, se o otimizador de consulta não puder fazer seu trabalho, há algo que posso fazer (ou parar de fazer) para ajudá-lo sem dizer explicitamente o que fazer.
Então, por que o SQL Server apresenta um plano melhor quando inline a variável?
No SQL Server, existem três formas comuns de predicado sem associação:
Com um valor literal :
Com um parâmetro :
Com uma variável local :
Resultados
Quando você usa um valor literal , e seu plano não é a) Trivial eb) Simples Parametrizado 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 criará um plano para esse parâmetro (isso é chamado de sniffing de parâmetro ) e, em seguida, reutilizará esse plano, dicas de recompilação ausentes, despejo de cache do plano etc.
Quando você usa uma variável local , o otimizador faz um plano para... Algo .
Se você fosse executar esta consulta:
O plano ficaria assim:
E o número estimado de linhas para essa variável local ficaria assim:
Mesmo que a consulta retorne uma contagem de 4.744.427.
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.
SELECT 5.280389E-05 * 7250739 AS [poo]
Isso lhe dará
382.86722457471
, que é o palpite que o otimizador faz.Essas suposições desconhecidas geralmente são suposições muito ruins e muitas vezes podem levar a planos ruins e escolhas de índice ruins.
Consertando-o?
Suas opções geralmente são:
Suas opções são especificamente:
Melhorar o índice atual significa estendê-lo para cobrir todas as colunas necessárias para a consulta:
Supondo que
Id
os valores sejam razoavelmente seletivos, isso lhe dará 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:
Vou supor que você tenha dados distorcidos, que não deseja usar dicas de consulta para forçar o otimizador a fazer o que fazer e que precisa obter um bom desempenho para todos os valores de entrada possíveis de
@Id
. Você pode obter um plano de consulta garantido para exigir apenas algumas leituras lógicas para qualquer valor de entrada possível se estiver disposto a criar o seguinte par de índices (ou seu equivalente):Abaixo estão meus dados de teste. Coloquei 13 M linhas na tabela e fiz metade delas ter um valor de
'3A35EA17-CE7E-4637-8319-4C517B6E48CA'
para aId
coluna.Esta consulta pode parecer um pouco estranha no início:
Ele foi projetado para aproveitar a ordenação dos índices para encontrar o valor mínimo ou máximo com algumas leituras lógicas. O
CROSS JOIN
existe para obter resultados corretos quando não há linhas correspondentes para o@Id
valor. Mesmo se eu filtrar o valor mais popular na tabela (correspondendo a 6,5 milhões de linhas), recebo apenas 8 leituras lógicas:Aqui está o plano de consulta:
Ambas as buscas de índice encontram 0 ou 1 linhas. É extremamente eficiente, mas criar dois índices pode ser um exagero para o seu cenário. Você pode considerar o seguinte índice em vez disso:
Agora, o plano de consulta para a consulta original (com uma
MAXDOP 1
dica opcional) parece um pouco diferente:As pesquisas de chave não são mais necessárias. Com um caminho de acesso melhor que deve funcionar bem para todas as entradas, você não precisa se preocupar com o otimizador escolhendo o plano de consulta errado devido ao vetor de densidade. No entanto, essa consulta e índice não serão tão eficientes quanto os outros se você procurar um
@Id
valor popular.Não posso responder por que aqui, mas a maneira rápida e suja de garantir que a consulta seja executada da maneira que você deseja é:
Isso incorre no risco de que a tabela ou os índices possam mudar no futuro, de modo que essa otimização se torne disfuncional, mas está disponível se você precisar. Espero que alguém possa oferecer uma resposta de causa raiz, conforme solicitado, em vez desta solução alternativa.