Sumário
Os principais problemas são:
- A seleção do plano do otimizador assume uma distribuição uniforme de valores.
- A falta de índices adequados significa:
- Digitalizar a tabela é a única opção.
- A junção é uma junção de loops aninhados ingênuos , em vez de uma junção de loops aninhados de índice . Em uma junção ingênua, os predicados da junção são avaliados na junção, em vez de serem empurrados para o lado interno da junção.
Detalhes
Os dois planos são fundamentalmente bastante semelhantes, embora o desempenho possa ser muito diferente:
Planejar com as colunas extras
Tomando o primeiro com as colunas extras que não são concluídas em um tempo razoável primeiro:
Os recursos interessantes são:
- A parte superior no nó 0 limita as linhas retornadas a 100. Também define uma meta de linha para o otimizador, para que tudo abaixo dele no plano seja escolhido para retornar as primeiras 100 linhas rapidamente.
- A Varredura no nó 4 localiza linhas da tabela em que
Start_Time
não é nulo, State
é 3 ou 4 e Operation_Type
é um dos valores listados. A tabela é totalmente varrida uma vez, com cada linha sendo testada com relação aos predicados mencionados. Somente as linhas que passam em todos os testes fluem para a Classificação. O otimizador estima que 38.283 linhas serão qualificadas.
- A classificação no nó 3 consome todas as linhas da verificação no nó 4 e as classifica em ordem de
Start_Time DESC
. Essa é a ordem de apresentação final solicitada pela consulta.
- O otimizador estima que 93 linhas (atualmente 93.2791) terão que ser lidas na Classificação para que todo o plano retorne 100 linhas (considerando o efeito esperado da associação).
- Espera-se que a junção de loops aninhados no nó 2 execute sua entrada interna (a ramificação inferior) 94 vezes (na verdade 94.2791). A linha extra é requerida pela troca de paralelismo de parada no nó 1 por razões técnicas.
- A Varredura no nó 5 varre completamente a tabela em cada iteração. Ele localiza linhas onde
Start_Time
não é nulo e State
é 3 ou 4. Estima-se que produz 400.875 linhas em cada iteração. Mais de 94.2791 iterações, o número total de linhas é quase 38 milhões.
- A junção de loops aninhados no nó 2 também aplica os predicados de junção. Ele verifica se há
Operation_Type
correspondência, se o Start_Time
nó 4 é menor que o Start_Time
nó 5, se o Start_Time
nó 5 é menor que o Finish_Time
nó 4 e se os dois Id
valores não correspondem.
- O Gather Streams (interrompe a troca de paralelismo) no nó 1 mescla os fluxos ordenados de cada encadeamento até que 100 linhas tenham sido produzidas. A natureza de preservação da ordem da mesclagem em vários fluxos é o que requer a linha extra mencionada na etapa 5.
A grande ineficiência está obviamente nas etapas 6 e 7 acima. A varredura completa da tabela no nó 5 para cada iteração é apenas um pouco razoável, se ocorrer apenas 94 vezes como o otimizador prevê. O conjunto de comparações de ~ 38 milhões por linha no nó 2 também é um custo alto.
Fundamentalmente, a estimativa de meta de linha de 93/94 linhas também provavelmente está errada, pois depende da distribuição de valores. O otimizador assume distribuição uniforme na ausência de informações mais detalhadas. Em termos simples, isso significa que, se se espera que 1% das linhas da tabela se qualifiquem, o otimizador raciocina que, para encontrar uma linha correspondente, ele precisa ler 100 linhas.
Se você executou essa consulta até a conclusão (o que pode levar muito tempo), provavelmente descobrirá que muito mais que 93/94 linhas precisam ser lidas na Classificação para finalmente produzir 100 linhas. Na pior das hipóteses, a 100ª linha seria encontrada usando a última linha da Classificação. Supondo que a estimativa do otimizador no nó 4 esteja correta, isso significa executar a verificação no nó 5 38.284 vezes, para um total de algo como 15 bilhões de linhas. Pode ser mais se as estimativas de digitalização também estiverem desativadas.
Este plano de execução também inclui um aviso de índice ausente:
/*
The Query Processor estimates that implementing the following index
could improve the query cost by 72.7096%.
WARNING: This is only an estimate, and the Query Processor is making
this recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide
impact, including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/
CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Batch_Tasks_Queue] ([Operation_Type],[State],[Start_Time])
INCLUDE ([Id],[Parameters])
O otimizador está alertando você para o fato de que adicionar um índice à tabela melhoraria o desempenho.
Planejar sem as colunas extras
Esse é essencialmente o mesmo plano que o anterior, com a adição do Index Spool no nó 6 e do Filtro no nó 5. As diferenças importantes são:
- O spool de índice no nó 6 é um spool ansioso. Ele ansiosamente consome o resultado da varredura abaixo dela, e constrói um índice temporário introduzidos no
Operation_Type
e Start_Time
, com Id
como uma coluna não-chave.
- A junção de loops aninhados no nó 2 agora é uma junção de índice. Sem predicados de junção são avaliados aqui, em vez dos valores atuais por iteração de
Operation_Type
, Start_Time
, Finish_Time
, e Id
da verificação no nó 4 são passados para o ramo do lado interno como referências externas.
- A verificação no nó 7 é realizada apenas uma vez.
- O spool de índice no nó 6 procura linhas do índice temporário onde
Operation_Type
corresponde ao valor de referência externa atual e Start_Time
está no intervalo definido pelas referências externas Start_Time
e Finish_Time
.
- O Filtro no nó 5 testa os
Id
valores do Spool do Índice quanto à desigualdade em relação ao valor de referência externa atual de Id
.
As principais melhorias são:
- A varredura lateral interna é realizada apenas uma vez
- Um índice temporário em (
Operation_Type
, Start_Time
) com Id
como uma coluna incluída permite a junção de loops aninhados ao índice. O índice é usado para procurar linhas correspondentes em cada iteração, em vez de varrer a tabela inteira a cada vez.
Como antes, o otimizador inclui um aviso sobre um índice ausente:
/*
The Query Processor estimates that implementing the following index
could improve the query cost by 24.1475%.
WARNING: This is only an estimate, and the Query Processor is making
this recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide
impact, including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/
CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Batch_Tasks_Queue] ([State],[Start_Time])
INCLUDE ([Id],[Operation_Type])
GO
Conclusão
O plano sem as colunas extras é mais rápido porque o otimizador optou por criar um índice temporário para você.
O plano com as colunas extras tornaria o índice temporário mais caro de construir. A [Parameters
coluna] é nvarchar(2000)
, que adicionaria até 4000 bytes a cada linha do índice. O custo adicional é suficiente para convencer o otimizador de que a criação do índice temporário em cada execução não se pagaria.
O otimizador avisa nos dois casos que um índice permanente seria uma solução melhor. A composição ideal do índice depende da sua carga de trabalho mais ampla. Para essa consulta específica, os índices sugeridos são um ponto de partida razoável, mas você deve entender os benefícios e custos envolvidos.
Recomendação
Uma ampla variedade de índices possíveis seria benéfica para esta consulta. O ponto importante é que é necessário algum tipo de índice não clusterizado. A partir das informações fornecidas, um índice razoável na minha opinião seria:
CREATE NONCLUSTERED INDEX i1
ON dbo.Batch_Tasks_Queue (Start_Time DESC)
INCLUDE (Operation_Type, [State], Finish_Time);
Eu também ficaria tentado a organizar a consulta um pouco melhor e demoraria a procurar as [Parameters]
colunas largas no índice clusterizado até depois que as 100 principais linhas fossem encontradas (usando Id
como chave):
SELECT TOP (100)
BTQ1.id,
BTQ2.id,
BTQ3.[Parameters],
BTQ4.[Parameters]
FROM dbo.Batch_Tasks_Queue AS BTQ1
JOIN dbo.Batch_Tasks_Queue AS BTQ2 WITH (FORCESEEK)
ON BTQ2.Operation_Type = BTQ1.Operation_Type
AND BTQ2.Start_Time > BTQ1.Start_Time
AND BTQ2.Start_Time < BTQ1.Finish_Time
AND BTQ2.id != BTQ1.id
-- Look up the [Parameters] values
JOIN dbo.Batch_Tasks_Queue AS BTQ3
ON BTQ3.Id = BTQ1.Id
JOIN dbo.Batch_Tasks_Queue AS BTQ4
ON BTQ4.Id = BTQ2.Id
WHERE
BTQ1.[State] IN (3, 4)
AND BTQ2.[State] IN (3, 4)
AND BTQ1.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
AND BTQ2.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
-- These predicates are not strictly needed
AND BTQ1.Start_Time IS NOT NULL
AND BTQ2.Start_Time IS NOT NULL
ORDER BY
BTQ1.Start_Time DESC;
Onde as [Parameters]
colunas não são necessárias, a consulta pode ser simplificada para:
SELECT TOP (100)
BTQ1.id,
BTQ2.id
FROM dbo.Batch_Tasks_Queue AS BTQ1
JOIN dbo.Batch_Tasks_Queue AS BTQ2 WITH (FORCESEEK)
ON BTQ2.Operation_Type = BTQ1.Operation_Type
AND BTQ2.Start_Time > BTQ1.Start_Time
AND BTQ2.Start_Time < BTQ1.Finish_Time
AND BTQ2.id != BTQ1.id
WHERE
BTQ1.[State] IN (3, 4)
AND BTQ2.[State] IN (3, 4)
AND BTQ1.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
AND BTQ2.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
AND BTQ1.Start_Time IS NOT NULL
AND BTQ2.Start_Time IS NOT NULL
ORDER BY
BTQ1.Start_Time DESC;
A FORCESEEK
dica está aqui para ajudar a garantir que o otimizador escolha um plano de loops aninhados indexados (existe uma tentação baseada em custo para o otimizador selecionar uma junção de hash ou (muitos) muitos), caso contrário, o que tende a não funcionar bem com esse tipo de Na prática, ambos acabam com grandes resíduos; muitos itens por balde no caso do hash e muitos rebobinamentos para a mesclagem).
Alternativa
Se a consulta (incluindo seus valores específicos) fosse particularmente crítica para o desempenho da leitura, consideraria dois índices filtrados:
CREATE NONCLUSTERED INDEX i1
ON dbo.Batch_Tasks_Queue (Start_Time DESC)
INCLUDE (Operation_Type, [State], Finish_Time)
WHERE
Start_Time IS NOT NULL
AND [State] IN (3, 4)
AND Operation_Type <> 23
AND Operation_Type <> 24
AND Operation_Type <> 25
AND Operation_Type <> 26
AND Operation_Type <> 27
AND Operation_Type <> 28
AND Operation_Type <> 30;
CREATE NONCLUSTERED INDEX i2
ON dbo.Batch_Tasks_Queue (Operation_Type, [State], Start_Time)
WHERE
Start_Time IS NOT NULL
AND [State] IN (3, 4)
AND Operation_Type <> 23
AND Operation_Type <> 24
AND Operation_Type <> 25
AND Operation_Type <> 26
AND Operation_Type <> 27
AND Operation_Type <> 28
AND Operation_Type <> 30;
Para a consulta que não precisa da [Parameters]
coluna, o plano estimado usando os índices filtrados é:
A varredura de índice retorna automaticamente todas as linhas qualificadas sem avaliar nenhum predicado adicional. Para cada iteração da junção de loops aninhados do índice, a busca do índice executa duas operações de busca:
- Um prefixo de busca corresponde a
Operation_Type
e State
= 3, buscando a faixa de Start_Time
valores, predicado residual da Id
desigualdade.
- Um prefixo de busca corresponde a
Operation_Type
e State
= 4, buscando a faixa de Start_Time
valores, predicado residual da Id
desigualdade.
Onde a [Parameters]
coluna é necessária, o plano de consulta simplesmente adiciona no máximo 100 pesquisas singleton para cada tabela:
Como nota final, considere usar os tipos inteiros padrão incorporados em vez de numeric
onde aplicável.