Obter uma digitalização, embora eu espere uma busca

9

Preciso otimizar uma SELECTinstrução, mas o SQL Server sempre faz uma verificação de índice em vez de uma busca. Esta é a consulta que, é claro, está em um procedimento armazenado:

CREATE PROCEDURE dbo.something
  @Status INT = NULL,
  @IsUserGotAnActiveDirectoryUser BIT = NULL    
AS

    SELECT [IdNumber], [Code], [Status], [Sex], 
           [FirstName], [LastName], [Profession], 
           [BirthDate], [HireDate], [ActiveDirectoryUser]
    FROM Employee
    WHERE (@Status IS NULL OR [Status] = @Status)
    AND 
    (
      @IsUserGotAnActiveDirectoryUser IS NULL 
      OR 
      (
        @IsUserGotAnActiveDirectoryUser IS NOT NULL AND       
        (
          @IsUserGotAnActiveDirectoryUser = 1 AND ActiveDirectoryUser <> ''
        )
        OR
        (
          @IsUserGotAnActiveDirectoryUser = 0 AND ActiveDirectoryUser = ''
        )
      )
    )

E este é o índice:

CREATE INDEX not_relevent ON dbo.Employee
(
    [Status] DESC,
    [ActiveDirectoryUser] ASC
)
INCLUDE (...all the other columns in the table...); 

O plano:

Plano de imagem

Por que o SQL Server escolheu uma verificação? Como posso corrigir isso?

Definições de coluna:

[Status] int NOT NULL
[ActiveDirectoryUser] VARCHAR(50) NOT NULL

Os parâmetros de status podem ser:

NULL: all status,
1: Status= 1 (Active employees)
2: Status = 2 (Inactive employees)

IsUserGotAnActiveDirectoryUser pode ser:

NULL: All employees
0: ActiveDirectoryUser is empty for that employee
1: ActiveDirectoryUser  got a valid value (not null and not empty)
Bestter
fonte
Você pode postar o plano de execução real em algum lugar (não uma imagem dele, mas o arquivo .sqlplan no formato XML)? Meu palpite é que você alterou o procedimento, mas na verdade não recebeu uma nova compilação no nível da instrução. Você pode alterar algum texto da consulta (como adicionar o prefixo do esquema ao nome da tabela ) e passar um valor válido para @Status?
Aaron Bertrand
11
A definição de índice também levanta a questão - por que a chave está ativada Status DESC? Quantos valores existem Status, quais são eles (se o número é pequeno) e cada valor é representado aproximadamente da mesma forma? Mostre-nos a saída deSELECT TOP (20) [Status], c = COUNT(*) FROM dbo.Employee GROUP BY [Status] ORDER BY c DESC;
Aaron Bertrand

Respostas:

11

Não acho que a verificação seja causada por uma pesquisa por uma string vazia (e, embora você possa adicionar um índice filtrado para esse caso, isso ajudará apenas variações muito específicas da consulta). É mais provável que você seja vítima do sniffing de parâmetros e de um único plano não otimizado para todas as várias combinações de parâmetros (e valores de parâmetros) que você fornecerá para esta consulta.

Eu chamo isso de procedimento "pia da cozinha" , porque você espera que uma consulta forneça todas as coisas, incluindo a pia da cozinha.

Eu tenho um vídeo sobre minha solução para isso aqui , mas essencialmente, a melhor experiência que tenho para essas consultas é:

  • Crie a declaração dinamicamente - isso permitirá que você deixe de fora as cláusulas que mencionam colunas para as quais nenhum parâmetro foi fornecido e garante que você tenha um plano otimizado precisamente para os parâmetros reais que foram passados ​​com valores.
  • UsoOPTION (RECOMPILE) - isso evita que valores específicos de parâmetros forcem o tipo errado de plano, especialmente útil quando você tem inclinação de dados, estatísticas incorretas ou quando a primeira execução de uma instrução usa um valor atípico que levará a um plano diferente do que mais tarde e mais frequente execuções.
  • Use a opção do servidoroptimize for ad hoc workloads - isso evita que variações de consulta usadas apenas uma vez poluam o cache do plano.

Ative a otimização para cargas de trabalho ad hoc:

EXEC sys.sp_configure 'show advanced options', 1;
GO
RECONFIGURE WITH OVERRIDE;
GO
EXEC sys.sp_configure 'optimize for ad hoc workloads', 1;
GO
RECONFIGURE WITH OVERRIDE;
GO
EXEC sys.sp_configure 'show advanced options', 0;
GO
RECONFIGURE WITH OVERRIDE;

Mude seu procedimento:

ALTER PROCEDURE dbo.Whatever
  @Status INT = NULL,
  @IsUserGotAnActiveDirectoryUser BIT = NULL
AS
BEGIN 
  SET NOCOUNT ON;
  DECLARE @sql NVARCHAR(MAX) = N'SELECT [IdNumber], [Code], [Status], 
     [Sex], [FirstName], [LastName], [Profession],
     [BirthDate], [HireDate], [ActiveDirectoryUser]
   FROM dbo.Employee -- please, ALWAYS schema prefix
   WHERE 1 = 1';

   IF @Status IS NOT NULL
     SET @sql += N' AND ([Status]=@Status)'

   IF @IsUserGotAnActiveDirectoryUser = 1
     SET @sql += N' AND ActiveDirectoryUser <> ''''';
   IF @IsUserGotAnActiveDirectoryUser = 0
     SET @sql += N' AND ActiveDirectoryUser = ''''';

   SET @sql += N' OPTION (RECOMPILE);';

   EXEC sys.sp_executesql @sql, N'@Status INT, @Status;
END
GO

Depois de ter uma carga de trabalho com base nesse conjunto de consultas que você pode monitorar, você pode analisar as execuções e ver quais delas se beneficiariam mais de índices adicionais ou diferentes - você pode fazer isso de vários ângulos, desde simples "qual combinação de parâmetros são fornecidos com mais frequência? " para "quais consultas individuais têm tempos de execução mais longos?" Não podemos responder a essas perguntas com base apenas no seu código, apenas podemos sugerir que qualquer índice seja útil apenas para um subconjunto de todas as combinações possíveis de parâmetros que você está tentando oferecer suporte. Por exemplo, se@Statusé NULL, nenhuma busca nesse índice não agrupado é possível. Portanto, nos casos em que os usuários não se importam com o status, você fará uma varredura, a menos que tenha um índice que atenda às outras cláusulas (mas esse índice também não será útil, dada a sua lógica de consulta atual - cadeia vazia ou cadeia vazia não é exatamente seletiva).

Nesse caso, dependendo do conjunto de Statusvalores possíveis e da distribuição desses valores, OPTION (RECOMPILE)pode não ser necessário. Mas se você tiver alguns valores que renderão 100 linhas e alguns que renderão centenas de milhares, convém que ele esteja lá (mesmo com o custo da CPU, que deve ser marginal, dada a complexidade dessa consulta), para que você possa obter buscas no maior número de casos possível. Se o intervalo de valores for finito o suficiente, você pode até fazer algo complicado com o SQL dinâmico, onde diz "Eu tenho esse valor muito seletivo para @Status; portanto, quando esse valor específico for passado, faça essa ligeira alteração no texto da consulta para que isso é considerado uma consulta diferente e otimizado para esse valor de parâmetro ".

Aaron Bertrand
fonte
3
Eu usei essa abordagem várias vezes e é uma maneira fantástica de fazer com que o otimizador faça as coisas da maneira que você acha que deve fazê-lo de qualquer maneira. Kim Tripp fala sobre uma solução semelhante aqui: sqlskills.com/blogs/kimberly/high-performance-procedures E tem um vídeo de uma sessão que ela fez no PASS há alguns anos atrás, que realmente entra em detalhes malucos sobre por que funciona. Dito isto, realmente não acrescenta muito ao que Bertrand disse aqui. Essa é uma daquelas ferramentas que todos devem manter no cinto de ferramentas. Realmente pode salvar algumas dores enormes para essas consultas abrangentes.
precisa saber é
3

Isenção de responsabilidade : algumas das coisas nesta resposta podem fazer um DBA recuar. Estou abordando isso do ponto de vista do desempenho puro - como obter pesquisas de índice quando você sempre obtém verificações de índice.

Com isso fora do caminho, aqui vai.

Sua consulta é conhecida como "consulta de pia da cozinha" - uma consulta única destinada a atender a várias condições possíveis de pesquisa. Se o usuário definir @statusum valor, você deseja filtrar esse status. Se @statusestiver NULL, retorne todos os status e assim por diante.

Isso apresenta problemas com a indexação, mas eles não estão relacionados à capacidade de Sargability, porque todas as suas condições de pesquisa são "iguais a" critérios.

Isso é sargável:

WHERE [status]=@status

Isso não é sargável porque o SQL Server precisa avaliar ISNULL([status], 0)para cada linha em vez de procurar um único valor no índice:

WHERE ISNULL([status], 0)=@status

Recriei o problema da pia da cozinha de uma forma mais simples:

CREATE TABLE #work (
    A    int NOT NULL,
    B    int NOT NULL
);

CREATE UNIQUE INDEX #work_ix1 ON #work (A, B);

INSERT INTO #work (A, B)
VALUES (1,  1), (2,  1),
       (3,  1), (4,  1),
       (5,  2), (6,  2),
       (7,  2), (8,  3),
       (9,  3), (10, 3);

Se você tentar o seguinte, você obterá uma verificação de índice, mesmo que A seja a primeira coluna do índice:

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE (@a IS NULL OR @a=A) AND
      (@b IS NULL OR @b=B);

Isso, no entanto, produz uma busca por índice:

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE @a=A AND
      @b IS NULL;

Enquanto você estiver usando uma quantidade gerenciável de parâmetros (dois no seu caso), provavelmente poderá fazer UNIONvárias consultas de pesquisa - basicamente todas as permutações dos critérios de pesquisa. Se você tiver três critérios, isso parecerá confuso; com quatro, será completamente incontrolável. Voce foi avisado.

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE @a=A AND
      @b IS NULL
UNION ALL
SELECT *
FROM #work
WHERE @a=A AND
      @b=B
UNION ALL
SELECT *
FROM #work
WHERE @a IS NULL AND
      @b=B
UNION ALL
SELECT *
FROM #work
WHERE @a IS NULL AND
      @b IS NULL;

Para o terceiro desses quatro usar uma busca de índice, você precisará de um segundo índice (B, A). Veja como sua consulta pode parecer com essas alterações (incluindo minha refatoração da consulta para torná-la mais legível).

DECLARE @Status int = NULL,
        @IsUserGotAnActiveDirectoryUser bit = NULL;

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser IS NULL

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser=1 AND ActiveDirectoryUser<>''

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser=0 AND (ActiveDirectoryUser IS NULL OR ActiveDirectoryUser='')

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser IS NULL

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser=1 AND ActiveDirectoryUser<>''

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser=0 AND (ActiveDirectoryUser IS NULL OR ActiveDirectoryUser='');

... mais você precisará de um índice adicional Employeecom as duas colunas de índice invertidas.

Para completar, devo mencionar que x=@ximplicitamente significa que xnão pode ser NULLporque NULLnunca é igual a NULL. Isso simplifica um pouco a consulta.

E sim, a resposta SQL dinâmica de Aaron Bertrand é uma escolha melhor na maioria dos casos (ou seja, sempre que você pode conviver com as recompilações).

Daniel Hutmacher
fonte
3

Sua pergunta básica parece ser "Por que" e acho que você pode encontrar a resposta sobre o minuto 55 ou mais desta ótima apresentação de Adam Machanic na TechEd há alguns anos atrás.

Menciono os 5 minutos no minuto 55, mas toda a apresentação vale o tempo. Se você olhar para o plano de consulta da sua consulta, tenho certeza de que encontrará Predicados Residuais para a pesquisa. Basicamente, o SQL não pode "ver" todas as partes do índice porque algumas delas estão ocultas pelas desigualdades e outras condições. O resultado é uma varredura de índice para um superconjunto baseado no Predicado. Esse resultado é colocado em spool e, em seguida, verificado novamente usando o predicado residual.

Verifique as propriedades do Operador de digitalização (F4) e veja se você tem "Seek Predicate" e "Predicate" na lista de propriedades.

Como outros indicaram, é difícil indexar a consulta como está. Eu tenho trabalhado em muitos similares recentemente e cada um deles exigiu uma solução diferente. :(

Raio
fonte
0

Antes de questionarmos se a busca de índice é preferível à varredura de índice, uma regra geral é verificar quantas linhas são retornadas versus o total de linhas da tabela subjacente. Por exemplo, se você espera que sua consulta retorne 10 linhas de 1 milhão de linhas, a busca de índice provavelmente é altamente preferida à varredura de índice. No entanto, se alguns milhares de linhas (ou mais) devem ser retornados da consulta, a busca por índice NÃO pode necessariamente ser preferida.

Sua consulta não é complexa; portanto, se você puder publicar um plano de execução, poderemos ter idéias melhores para ajudá-lo.

jyao
fonte
Filtrando alguns milhares de linhas de uma tabela de 1 milhão, eu ainda gostaria de procurar - ainda é uma grande melhoria de desempenho em relação à varredura de toda a tabela.
Daniel Hutmacher
-6

este é apenas o original formatado

DECLARE @Status INT = NULL,
        @IsUserGotAnActiveDirectoryUser BIT = NULL    

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName], [Profession],
       [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE (@Status IS NULL OR [Status]=@Status)  
AND (            @IsUserGotAnActiveDirectoryUser IS NULL 
      OR (       @IsUserGotAnActiveDirectoryUser IS NOT NULL 
           AND (     @IsUserGotAnActiveDirectoryUser = 1 
                 AND ActiveDirectoryUser <> '') 
           OR  (     @IsUserGotAnActiveDirectoryUser = 0 
                 AND ActiveDirectoryUser =  '')
         )
    )

esta é a revisão - não tenho 100% de certeza, mas (talvez) experimente,
mesmo uma OU provavelmente será um problema que
poderia ser interrompido no ActiveDirectoryUser null

  WHERE isnull(@Status, [Status]) = [Status]
    AND (      (     isnull(@IsUserGotAnActiveDirectoryUser, 1) = 1 
                 AND ActiveDirectoryUser <> '' ) 
           OR  (     isnull(@IsUserGotAnActiveDirectoryUser, 0) = 0 
                 AND ActiveDirectoryUser =  '' )
        )
paparazzo
fonte
3
Não está claro para mim como essa resposta resolve a pergunta do OP.
Erik
@Erik Poderíamos gostar de deixar o OP experimentá-lo? Dois OU foram embora. Você tem certeza de que isso não pode ajudar a consultar o desempenho?
Paparazzo
@ ypercubeᵀᴹ IsUserGotAnActiveDirectoryUser NÃO É NULL é removido. Esses dois desnecessários removem um OR e removem IsUserGotAnActiveDirectoryUser IS NULL. Você tem certeza de que esta consulta não será executada rapidamente, em seguida, o OP?
paparazzo
@ ypercubeᵀᴹ Poderia ter feito muitas coisas. Eu não estou procurando mais simples. Dois ou se foram. Ou normalmente é ruim para planos de consulta. Eu chego lá é uma espécie de clube aqui e não faço parte do clube. Mas faço isso para viver e postar o que sei que funcionou. Minhas respostas não são afetadas por votos negativos.
paparazzo