Eu tenho uma consulta problemática que estamos tentando ajustar. Um dos nossos primeiros pensamentos foi pegar uma parte de um plano de execução maior e armazenar esses resultados em uma tabela temporária intermediária e, em seguida, realizar as outras operações.
O que estou observando é que, quando inserimos os dados em uma tabela temporária, o custo do plano de execução aumenta (22 -> 1,1k). Agora, isso tem o benefício de permitir que o plano seja paralelo, o que reduziu o tempo de execução em 20%, mas não vale o uso muito maior de CPU por execução em nosso caso.
Estamos usando o SQL Server 2016 SP2 com o CE legado ativado.
Plano original (custo ~ 20):
https://www.brentozar.com/pastetheplan/?id=ry-QGnkCM
SQL original:
WITH Object1(Column1, Column2, Column3, Column4, Column5, Column6)
AS
(
SELECT Object2.Column1,
Object2.Column2,
Object3.Column3,
Object3.Column4,
Object3.Column5,
Object3.Column6
FROM Object4 AS Object5
INNER JOIN Object6 AS Object2 ON Object2.Column2 = Object5.Column2 AND Object2.Column7 = 0
INNER JOIN Object7 AS Object8 ON Object8.Column8 = Object2.Column9 AND Object8.Column7 = 0
INNER JOIN Object9 AS Object3 ON Object3.Column10 = Object8.Column11 AND Object3.Column7 = 0
INNER JOIN Object10 AS Object11 ON Object2.Column1 = Object11.Column1
WHERE Object8.Column12 IS NULL AND
Object8.Column13 = Object5.Column13 AND
Object3.Column3 = Object5.Column3 AND
Object11.Column14 = Variable1
)
insert Object12
SELECT Object13.Column2,
Object13.Column3,
MIN(Object13.Column4) AS Column15,
MAX(Object13.Column4) AS Column16,
COUNT(DISTINCT (CASE WHEN Object13.Column5 = 1 THEN Object13.Column1 END)) AS Column17,
COUNT(DISTINCT (CASE WHEN Object13.Column6 = 0 THEN Object13.Column1 END)) AS Column18,
COUNT(DISTINCT Object13.Column1) AS Column19
FROM Object1 AS Object13
GROUP BY Object13.Column2, Object13.Column3 OPTION (RECOMPILE)
Novo plano (com a área destacada em azul acima é pré-preparado em uma tabela temporária - Custo ~ 1,1k):
https://www.brentozar.com/pastetheplan/?id=rycqG3JRf
Novo SQL:
SELECT Object1.Column1,
Object1.Column2,
MIN(Object2.Column3) AS Column4,
MAX(Object2.Column3) AS Column5,
COUNT(DISTINCT (CASE WHEN Object2.Column6 = 1 THEN Object1.Column7 END)) AS Column8,
COUNT(DISTINCT (CASE WHEN Object2.Column9 = 0 THEN Object1.Column7 END)) AS Column10,
COUNT(DISTINCT Object1.Column7) AS Column11
from Object3 Object1
join Object4 Object2 on Object2.Column12 = Object1.Column13 and Object2.Column2 = Object1.Column2
where Object2.Column14 = 0
GROUP BY Object1.Column1, Object1.Column2 OPTION (RECOMPILE)
Alguém pode nos ajudar a entender porque o novo plano teria um custo tão maior? Ficarei feliz em fornecer informações adicionais sobre tabelas/índices abaixo, se necessário.
No caso do plano original, percebemos que ele está fazendo uma inserção em vez de uma seleção. Mesmo assim, o select não deveria (na nossa cabeça) ser muito mais caro.
Este é o plano de execução real. É uma preocupação porque, devido ao custo do plano imensamente mais alto, ele é paralelo. Portanto, usando CPU mais alta. Além disso, estamos curiosos para saber por que o custo do plano aumenta tanto para algo como pré-encenar os dados, o que geralmente o aproxima, se não melhor, do custo original.
A tabela temporária é indexada na segunda consulta como um PK clusterizado composto em Object1.Column13 e Object1.Column2. Isso corresponde às colunas (e ordem) do Object4. Adicionar uma MAXDOP
dica é uma opção, mas isso também é um exercício acadêmico de 'por que no mundo o custo sobe tanto'?
Adicionar OPTION (ORDER GROUP)
à segunda consulta resulta em nenhuma alteração, mesmos operadores/custos.
NOTAS:
- Object9 na primeira consulta é o mesmo objeto que Object4 na segunda.
Os custos são baseados em estimativas, mesmo em "planos reais". Você não pode comparar dois planos de consulta lado a lado e concluir que um deles exigirá mais CPU para executar com base apenas no operador ou nos custos totais do plano. Posso criar uma consulta com um custo na casa dos milhões que é executada em um segundo. Também posso criar uma consulta com um custo minúsculo que efetivamente levará uma eternidade para ser executada. Para o seu caso, a primeira consulta tem um custo de apenas 22 unidades do otimizador devido a uma estimativa de cardinalidade ruim após a junção de hash:
Os operadores em vermelho executam milhões de vezes, mas o otimizador de consulta espera que eles executem apenas alguns milhares de vezes. Os custos, que são baseados em estimativas, não refletem esse trabalho. O operador em azul é um carretel de tabela para o qual o estimador de cardinalidade espera inserir uma única linha. Em vez disso, insere alguns milhões. Como resultado, os operadores em preto (junto com alguns outros não mostrados) são ineficientes e se espalham para o tempdb.
Com o outro plano, você coloca um número significativo de linhas em tempdb e, como resultado, a estimativa de cardinalidade é mais razoável, embora ainda não seja ideal:
O otimizador de consulta espera precisar processar muito mais linhas e, como resultado, o plano de consulta tem um custo mais alto. Como regra geral, você pode ver um desempenho aprimorado com estimativas aprimoradas, mas nem sempre funciona como você gostaria. Olhando para o plano com uma tabela temporária, vejo algumas áreas de possível melhoria:
Carregue o CTE completo da consulta original na tabela temporária. Uma consulta com vários agregados distintos pode ser difícil de otimizar. Às vezes, você obtém um plano de consulta em que todos os dados são carregados em um spool (no tempdb) e alguns dos agregados são aplicados separadamente ao spool. Todo esse trabalho é sempre feito em uma zona serial na minha experiência. Se você eliminar todas as junções na consulta, acredito que não obterá essa otimização. Os agregados serão aplicados apenas à tabela temporária. Isso economizará o trabalho de escrever quase os mesmos dados para tempdb e todo o plano deve ser elegível para paralelismo.
Defina a tabela temporária como um heap e grave nela com
TABLOCK
. Parece que agora você tem um índice clusterizado, o que significa que você não está qualificado para inserção paralela.Considere tornar a consulta elegível para o modo em lote usando um desses truques . Os agregados do modo de lote podem ser significativamente mais eficientes com vários agregados distintos.
Eu esperaria que alguma combinação dessas etapas melhorasse significativamente o tempo de execução. Observe que fiz uma análise rápida parcialmente porque os planos anônimos são difíceis de interpretar.