Sqlserver
 sql >> Base de Dados >  >> RDS >> Sqlserver

Por que o SQL Server está usando a varredura de índice em vez da busca de índice quando a cláusula WHERE contém valores parametrizados


Bem, para responder à sua pergunta por que o SQL Server está fazendo isso, a resposta é que a consulta não é compilada em uma ordem lógica, cada instrução é compilada por seu próprio mérito, portanto, quando o plano de consulta para sua instrução select está sendo gerado, o otimizador não sabe que @val1 e @Val2 se tornarão 'val1' e 'val2' respectivamente.

Quando o SQL Server não sabe o valor, ele precisa adivinhar quantas vezes essa variável aparecerá na tabela, o que às vezes pode levar a planos abaixo do ideal. Meu ponto principal é que a mesma consulta com valores diferentes pode gerar planos diferentes. Imagine este exemplo simples:
IF OBJECT_ID(N'tempdb..#T', 'U') IS NOT NULL
    DROP TABLE #T;

CREATE TABLE #T (ID INT IDENTITY PRIMARY KEY, Val INT NOT NULL, Filler CHAR(1000) NULL);
INSERT #T (Val)
SELECT  TOP 991 1
FROM    sys.all_objects a
UNION ALL
SELECT  TOP 9 ROW_NUMBER() OVER(ORDER BY a.object_id) + 1
FROM    sys.all_objects a;

CREATE NONCLUSTERED INDEX IX_T__Val ON #T (Val);

Tudo o que fiz aqui foi criar uma tabela simples e adicionar 1000 linhas com valores de 1 a 10 para a coluna val , porém 1 aparece 991 vezes e os outros 9 aparecem apenas uma vez. A premissa é esta consulta:
SELECT  COUNT(Filler)
FROM    #T
WHERE   Val = 1;

Seria mais eficiente apenas escanear a tabela inteira, do que usar o índice para uma busca e fazer 991 pesquisas de favoritos para obter o valor de Filler , porém com apenas 1 linha a seguinte consulta:
SELECT  COUNT(Filler)
FROM    #T
WHERE   Val = 2;

será mais eficiente fazer uma busca de índice e uma única pesquisa de marcador para obter o valor de Filler (e executar essas duas consultas ratificará isso)

Tenho certeza de que o corte para uma pesquisa de busca e favoritos varia de acordo com a situação, mas é bastante baixo. Usando a tabela de exemplo, com um pouco de tentativa e erro, descobri que precisava do Val coluna para ter 38 linhas com o valor 2 antes que o otimizador fosse para uma varredura completa da tabela em uma busca de índice e pesquisa de favoritos:
IF OBJECT_ID(N'tempdb..#T', 'U') IS NOT NULL
    DROP TABLE #T;

DECLARE @I INT = 38;

CREATE TABLE #T (ID INT IDENTITY PRIMARY KEY, Val INT NOT NULL, Filler CHAR(1000) NULL);
INSERT #T (Val)
SELECT  TOP (991 - @i) 1
FROM    sys.all_objects a
UNION ALL
SELECT  TOP (@i) 2
FROM    sys.all_objects a
UNION ALL
SELECT  TOP 8 ROW_NUMBER() OVER(ORDER BY a.object_id) + 2
FROM    sys.all_objects a;

CREATE NONCLUSTERED INDEX IX_T__Val ON #T (Val);

SELECT  COUNT(Filler), COUNT(*)
FROM    #T
WHERE   Val = 2;

Portanto, para este exemplo, o limite é de 3,7% de linhas correspondentes.

Como a consulta não sabe quantas linhas corresponderão quando você estiver usando uma variável, ela precisa adivinhar, e a maneira mais simples é descobrir o número total de linhas e dividir isso pelo número total de valores distintos na coluna, então neste exemplo o número estimado de linhas para WHERE val = @Val é 1000 / 10 =100, O algoritmo real é mais complexo do que isso, mas, por exemplo, isso servirá. Então, quando olhamos para o plano de execução para:
DECLARE @i INT = 2;
SELECT  COUNT(Filler)
FROM    #T
WHERE   Val = @i;



Podemos ver aqui (com os dados originais) que o número estimado de linhas é 100, mas as linhas reais são 1. Das etapas anteriores, sabemos que com mais de 38 linhas o otimizador optará por uma varredura de índice clusterizado em vez de um índice search, portanto, como a melhor estimativa para o número de linhas é maior que isso, o plano para uma variável desconhecida é uma varredura de índice clusterizado.

Apenas para provar ainda mais a teoria, se criarmos a tabela com 1000 linhas de números de 1 a 27 distribuídas uniformemente (portanto, a contagem de linhas estimada será aproximadamente 1000 / 27 =37,037)
IF OBJECT_ID(N'tempdb..#T', 'U') IS NOT NULL
    DROP TABLE #T;

CREATE TABLE #T (ID INT IDENTITY PRIMARY KEY, Val INT NOT NULL, Filler CHAR(1000) NULL);
INSERT #T (Val)
SELECT  TOP 27 ROW_NUMBER() OVER(ORDER BY a.object_id)
FROM    sys.all_objects a;

INSERT #T (val)
SELECT  TOP 973 t1.Val
FROM    #T AS t1
        CROSS JOIN #T AS t2
        CROSS JOIN #T AS t3
ORDER BY t2.Val, t3.Val;

CREATE NONCLUSTERED INDEX IX_T__Val ON #T (Val);

Em seguida, execute a consulta novamente, obtemos um plano com uma busca de índice:
DECLARE @i INT = 2;
SELECT  COUNT(Filler)
FROM    #T
WHERE   Val = @i;



Espero que isso cubra de maneira bastante abrangente por que você obtém esse plano. Agora, suponho que a próxima pergunta seja como você força um plano diferente, e a resposta é usar a dica de consulta OPTION (RECOMPILE) , para forçar a compilação da consulta em tempo de execução quando o valor do parâmetro for conhecido. Revertendo para os dados originais, onde o melhor plano para Val = 2 é uma pesquisa, mas usando uma variável produz um plano com uma varredura de índice, podemos executar:
DECLARE @i INT = 2;
SELECT  COUNT(Filler)
FROM    #T
WHERE   Val = @i;

GO

DECLARE @i INT = 2;
SELECT  COUNT(Filler)
FROM    #T
WHERE   Val = @i
OPTION (RECOMPILE);



Podemos ver que este último utiliza o index seek e key lookup porque verificou o valor da variável em tempo de execução, e o plano mais adequado para aquele valor específico é escolhido. O problema com OPTION (RECOMPILE) é que isso significa que você não pode tirar proveito dos planos de consulta em cache, portanto, há um custo adicional de compilar a consulta a cada vez.