Como otimizar a consulta T-SQL usando o Plano de Execução

15

Eu tenho uma consulta SQL que passei nos últimos dois dias tentando otimizar usando tentativa e erro e o plano de execução, mas sem sucesso. Por favor, perdoe-me por fazer isso, mas postarei todo o plano de execução aqui. Fiz um esforço para tornar os nomes de tabela e coluna no plano de consulta e execução genéricos, por questões de brevidade e para proteger o IP da minha empresa. O plano de execução pode ser aberto com o SQL Sentry Plan Explorer .

Fiz uma boa quantidade de T-SQL, mas usar planos de execução para otimizar minha consulta é uma nova área para mim e realmente tentei entender como fazê-lo. Portanto, se alguém pudesse me ajudar com isso e explicar como esse plano de execução pode ser decifrado para encontrar maneiras na consulta de otimizá-lo, ficaria eternamente grato. Tenho muito mais consultas para otimizar - só preciso de um trampolim para me ajudar com essa primeira.

Esta é a consulta:

DECLARE @Param0 DATETIME     = '2013-07-29';
DECLARE @Param1 INT          = CONVERT(INT, CONVERT(VARCHAR, @Param0, 112))
DECLARE @Param2 VARCHAR(50)  = 'ABC';
DECLARE @Param3 VARCHAR(100) = 'DEF';
DECLARE @Param4 VARCHAR(50)  = 'XYZ';
DECLARE @Param5 VARCHAR(100) = NULL;
DECLARE @Param6 VARCHAR(50)  = 'Text3';

SET NOCOUNT ON

DECLARE @MyTableVar TABLE
(
    B_Var1_PK int,
    Job_Var1 varchar(512),
    Job_Var2 varchar(50)
)

INSERT INTO @MyTableVar (B_Var1_PK, Job_Var1, Job_Var2) 
SELECT B_Var1_PK, Job_Var1, Job_Var2 FROM [fn_GetJobs] (@Param1, @Param2, @Param3, @Param4, @Param6);

CREATE TABLE #TempTable
(
    TTVar1_PK INT PRIMARY KEY,
    TTVar2_LK VARCHAR(100),
    TTVar3_LK VARCHAR(50),
    TTVar4_LK INT,
    TTVar5 VARCHAR(20)
);

INSERT INTO #TempTable
SELECT DISTINCT
    T.T1_PK,
    T.T1_Var1_LK,
    T.T1_Var2_LK,
    MAX(T.T1_Var3_LK),
    T.T1_Var4_LK
FROM
    MyTable1 T
    INNER JOIN feeds.MyTable2 A ON A.T2_Var1 = T.T1_Var4_LK
    INNER JOIN @MyTableVar B ON B.Job_Var2 = A.T2_Var2 AND B.Job_Var1 = A.T2_Var3
GROUP BY T.T1_PK, T.T1_Var1_LK, T.T1_Var2_LK, T.T1_Var4_LK

-- This is the slow statement...
SELECT 
    CASE E.E_Var1_LK
        WHEN 'Text1' THEN T.TTVar2_LK + '_' + F.F_Var1
        WHEN 'Text2' THEN T.TTVar2_LK + '_' + F.F_Var2
        WHEN 'Text3' THEN T.TTVar2_LK
    END,
    T.TTVar4_LK,
    T.TTVar3_LK,
    CASE E.E_Var1_LK
        WHEN 'Text1' THEN F.F_Var1
        WHEN 'Text2' THEN F.F_Var2
        WHEN 'Text3' THEN T.TTVar5
    END,
    A.A_Var3_FK_LK,
    C.C_Var1_PK,
    SUM(CONVERT(DECIMAL(18,4), A.A_Var1) + CONVERT(DECIMAL(18,4), A.A_Var2))
FROM #TempTable T
    INNER JOIN TableA (NOLOCK) A ON A.A_Var4_FK_LK  = T.TTVar1_PK
    INNER JOIN @MyTableVar     B ON B.B_Var1_PK     = A.Job
    INNER JOIN TableC (NOLOCK) C ON C.C_Var2_PK     = A.A_Var5_FK_LK
    INNER JOIN TableD (NOLOCK) D ON D.D_Var1_PK     = A.A_Var6_FK_LK
    INNER JOIN TableE (NOLOCK) E ON E.E_Var1_PK     = A.A_Var7_FK_LK  
    LEFT OUTER JOIN feeds.TableF (NOLOCK) F ON F.F_Var1 = T.TTVar5
WHERE A.A_Var8_FK_LK = @Param1
GROUP BY
    CASE E.E_Var1_LK
        WHEN 'Text1' THEN T.TTVar2_LK + '_' + F.F_Var1
        WHEN 'Text2' THEN T.TTVar2_LK + '_' + F.F_Var2
        WHEN 'Text3' THEN T.TTVar2_LK
    END,
    T.TTVar4_LK,
    T.TTVar3_LK,
    CASE E.E_Var1_LK 
        WHEN 'Text1' THEN F.F_Var1
        WHEN 'Text2' THEN F.F_Var2
        WHEN 'Text3' THEN T.TTVar5
    END,
    A.A_Var3_FK_LK, 
    C.C_Var1_PK


IF OBJECT_ID(N'tempdb..#TempTable') IS NOT NULL
BEGIN
    DROP TABLE #TempTable
END
IF OBJECT_ID(N'tempdb..#TempTable') IS NOT NULL
BEGIN
    DROP TABLE #TempTable
END

O que descobri é que a terceira declaração (comentada como lenta) é a parte que está demorando mais tempo. As duas declarações anteriores retornam quase instantaneamente.

O plano de execução está disponível como XML neste link .

É melhor clicar com o botão direito do mouse e salvar e abrir no SQL Sentry Plan Explorer ou em algum outro software de visualização, em vez de abrir no navegador.

Se precisar de mais informações minhas sobre tabelas ou dados, não hesite em perguntar.

Neo
fonte
2
Suas estatísticas estão muito longe. Quando é a última vez que você des fragmenta índices ou atualiza estatísticas? Além disso, eu tentaria usar uma tabela temporária, em vez da variável da tabela, @MyTableVar, pois o otimizador realmente não pode usar estatísticas nas variáveis ​​da tabela.
Adam Haines
Obrigado pela sua resposta Adam. Alterar o @MyTableVar para uma tabela temporária não tem efeito, mas é apenas um pequeno número de linhas (que podem ser vistas no plano de execução). O que no plano de execução mostra que minhas estatísticas estão muito longe? Indica quais índices devem ser reorganizados ou reconstruídos e quais tabelas devem ter estatísticas atualizadas?
Neo
3
Essa junção de hash no canto inferior direito tem uma estimativa de 24.000 linhas na entrada de compilação, mas 3.285.620 reais podem estar sendo utilizadas tempdb. ou seja, as estimativas para as linhas resultantes da junção entre TableAe @MyTableVarestão muito distantes. Além disso, o número de linhas entrando nas classificações é muito maior do que o estimado, portanto elas também podem estar se espalhando.
Martin Smith

Respostas:

21

Antes de chegar à resposta principal, existem dois softwares que você precisa atualizar.

Atualizações de software necessárias

O primeiro é o SQL Server. Você está executando o SQL Server 2008 Service Pack 1 (compilação 2531). Você deve estar atualizado até pelo menos o Service Pack atual (SQL Server 2008 Service Pack 3 - build 5500). A versão mais recente do SQL Server 2008 no momento da redação deste artigo é o Service Pack 3, Atualização Cumulativa 12 (versão 5844).

O segundo software é o SQL Sentry Plan Explorer . As versões mais recentes têm novos recursos e correções significativos, incluindo a capacidade de fazer upload diretamente de um plano de consulta para análise especializada (não é necessário colar XML em qualquer lugar!)

Análise do plano de consulta

A estimativa de cardinalidade para a variável da tabela está exatamente correta, graças a uma recompilação no nível da instrução:

estimativa da variável de tabela

Infelizmente, as variáveis ​​de tabela não mantêm estatísticas de distribuição; portanto, o otimizador sabe que existem seis linhas; não conhece nada dos valores que possam estar nessas seis linhas. Essa informação é crucial, pois a próxima operação é uma junção a outra tabela. A estimativa de cardinalidade dessa associação é baseada em um palpite do otimizador:

primeira estimativa de junção

A partir daí, o plano escolhido pelo otimizador é baseado em informações incorretas, portanto, não é de admirar que o desempenho seja tão ruim. Em particular, a memória reservada para classificações e tabelas de hash para junções de hash será muito pequena. No tempo de execução, as operações de classificação e hash transbordantes serão derramadas para disco tempdb físico .

O SQL Server 2008 não destaca isso nos planos de execução; você pode monitorar os derramamentos usando Eventos estendidos ou Avisos de classificação do criador de perfil e Avisos de hash . A memória é reservada para classificações e hashes com base em estimativas de cardinalidade antes do início da execução e não pode ser aumentada durante a execução, independentemente da quantidade de memória disponível que o SQL Server possa ter. Portanto, estimativas precisas de contagem de linhas são cruciais para qualquer plano de execução que envolva operações que consomem memória do espaço de trabalho.

Sua consulta também é parametrizada. Você deve adicionar OPTION (RECOMPILE)à consulta se valores diferentes de parâmetros afetarem o plano de consulta. Você provavelmente deve considerar usá-lo de qualquer maneira, para que o otimizador possa ver o valor @Param1no momento da compilação. Se nada mais, isso pode ajudar o otimizador a produzir uma estimativa mais razoável para a busca de índice mostrada acima, dado que a tabela é muito grande e particionada. Também pode permitir a eliminação de partição estática.

Tente a consulta novamente com uma tabela temporária em vez da variável de tabela e OPTION (RECOMPILE) . Você também deve tentar materializar o resultado da primeira junção em outra tabela temporária e executar o restante da consulta. O número de linhas não é tão grande (3.285.620); portanto, isso deve ser razoavelmente rápido. O otimizador terá uma estimativa exata da cardinalidade e estatísticas de distribuição para o resultado da associação. Com sorte, o restante do plano se encaixará bem.

Trabalhando a partir das propriedades mostradas no plano, a consulta de materialização seria:

SELECT
    A.A_Var7_FK_LK,
    A.A_Var4_FK_LK,
    A.A_Var6_FK_LK, 
    A.A_Var5_FK_LK,
    A.A_Var1,
    A.A_Var2,
    A.A_Var3_FK_LK
INTO #AnotherTempTable
FROM @MyTableVar AS B
JOIN TableA AS A
    ON A.Job = B.B_Var1_PK
WHERE
    A_Var8_FK_LK = @Param1;

Você também pode INSERTinserir uma tabela temporária predefinida (os tipos de dados corretos não são mostrados no plano, portanto não posso fazer essa parte). A nova tabela temporária pode ou não se beneficiar de índices clusterizados e não clusterizados.

Paul White restabelece Monica
fonte
Muito obrigado por esta resposta detalhada. Desculpe, demorou uma semana para responder. Estive trabalhando nisso todos os dias, intercalado com outros trabalhos. Implementamos suas sugestões materializando a associação à TabelaA #AnotherTempTable. Isso pareceu ter o melhor impacto - as outras sugestões (o uso de uma tabela temporária em vez de uma variável de tabela para @MyTableVar e o uso OPTION (RECOMPILE)não tiveram muito efeito ou nenhum efeito. O 'Anonymize' e 'Post to SQLPerformance.com' as opções no SQL Sentry Plan Explorer são ótimas - eu apenas as usei: answers.sqlperformance.com/questions/1087
Neo
-6

Percebo que deve haver um PK em @MyTableVar e concordo que #MyTableVar geralmente tem melhor desempenho (principalmente com um número maior de linhas).

A condição na cláusula where

   WHERE A.A_Var8_FK_LK = @Param1

deve ser movido para a junção interna A AND'ed. O otimizador não é inteligente o suficiente na minha experiência para fazer isso (desculpe, não olhei para o plano) e pode fazer uma enorme diferença.

Se essas alterações não mostrarem melhoria, em seguida, criaria outra tabela temporária de A e todas as coisas às quais ela se restringe (agradavelmente?) Por A.A_Var8_FK_LK = @ Param1, se esse agrupamento fizer sentido lógico para você.

Em seguida, crie um índice em cluster nessa tabela temporária (antes ou depois da criação) para a próxima condição de associação.

Em seguida, junte esse resultado às poucas tabelas (F e T) que restam.

Bam, que precisa de um plano de consulta fedorento quando as estimativas de linha estão desativadas e às vezes não é facilmente improvável de qualquer maneira). Suponho que você tenha índices adequados, que é o que eu verificaria primeiro dentro do plano.

Um rastreamento pode mostrar os derramamentos de tempdb que podem ou não ter um impacto drástico.

Outra abordagem alternativa - que é mais rápida de tentar, pelo menos - é ordenar as tabelas do menor número de linhas (A) para o mais alto e começar a adicionar a mesclagem, o hash e o loop nas junções. Quando dicas estão presentes, a ordem de junção é fixada conforme especificado. Outros usuários evitam sabiamente essa abordagem porque ela pode prejudicar a longo prazo se a contagem relativa de linhas mudar drasticamente. Um número mínimo de dicas é desejável.

Se você estiver fazendo muitas dessas ações, talvez um otimizador comercial valha a pena tentar (ou testar) e ainda assim ser uma boa experiência de aprendizado.

crokusek
fonte
Sim, ele é. Ele garante que as linhas retornadas por A sejam limitadas pela restrição. Caso contrário, o otimizador poderá ingressar primeiro e aplicar a restrição posteriormente. Eu lido com esse diário.
crokusek
4
@crokusek Você está errado. O otimizador do SQL-Server é muito bom em saber que as consultas são equivalentes (se uma condição está na cláusula WHERE ou ON) quando é uma associação INNER.
precisa saber é o seguinte
6
Você pode achar útil a série de Paul White no Query Optimizer .
Martin Smith
É um péssimo hábito. Talvez seja esse caso em particular (onde há uma restrição), mas eu estou vindo da terra de vários desenvolvedores que estão empilhando as condições AND na cláusula where. SQL Server faz não consistentemente "movimento" de volta para a junção para você.
crokusek
Concordo incorreto para junções externas (e direita). Mas quando existem apenas expressões AND dentro de uma cláusula where e cada termo corresponde apenas a uma junção interna específica, esse termo pode ser movido com segurança e confiança para o local "on" como uma otimização e melhor prática (imo). Se é uma condição de união "verdadeira" ou apenas uma restrição fixa, é secundária a um grande ganho de desempenho. Esse link é para um caso trivial. A vida real tem várias condições em que os convert () e a matemática são os melhores candidatos para obter as melhores práticas.
crokusek