A consulta não responde ao adicionar duas colunas

9

Quando adiciono duas colunas à minha seleção, a consulta não responde. O tipo de coluna é nvarchar(2000). É um pouco incomum.

  • A versão do SQL Server é 2014.
  • Existe apenas um índice primário.
  • O registro inteiro tem apenas 1000 linhas.

Aqui está o plano de execução antes ( XML showplan ):

insira a descrição da imagem aqui

Plano de execução após ( XML showplan ):

insira a descrição da imagem aqui

Aqui está a consulta:

select top(100)
  Batch_Tasks_Queue.id,
  btq.id,
  Batch_Tasks_Queue.[Parameters], -- this field
  btq.[Parameters]  -- and this field
from
        Batch_Tasks_Queue with(nolock)
    inner join  Batch_Tasks_Queue btq with(nolock)  on  Batch_Tasks_Queue.Start_Time < btq.Start_Time
                            and btq.Start_Time < Batch_Tasks_Queue.Finish_Time
                            and Batch_Tasks_Queue.id <> btq.id                            
                            and btq.Start_Time is not null
                            and btq.State in (3, 4)                          
where
    Batch_Tasks_Queue.Start_Time is not null      
    and Batch_Tasks_Queue.State in (3, 4)
    and Batch_Tasks_Queue.Operation_Type = btq.Operation_Type
    and Batch_Tasks_Queue.Operation_Type not in (23, 24, 25, 26, 27, 28, 30)

order by
    Batch_Tasks_Queue.Start_Time desc

A contagem total de resultados é de 17 linhas. Os dados sujos (dica nolock) não são importantes.

Aqui está a estrutura da tabela:

CREATE TABLE [dbo].[Batch_Tasks_Queue](
    [Id] [int] NOT NULL,
    [OBJ_VERSION] [numeric](8, 0) NOT NULL,
    [Operation_Type] [numeric](2, 0) NULL,
    [Request_Time] [datetime] NOT NULL,
    [Description] [varchar](1000) NULL,
    [State] [numeric](1, 0) NOT NULL,
    [Start_Time] [datetime] NULL,
    [Finish_Time] [datetime] NULL,
    [Parameters] [nvarchar](2000) NULL,
    [Response] [nvarchar](max) NULL,
    [Billing_UserId] [int] NOT NULL,
    [Planned_Start_Time] [datetime] NULL,
    [Input_FileId] [uniqueidentifier] NULL,
    [Output_FileId] [uniqueidentifier] NULL,
    [PRIORITY] [numeric](2, 0) NULL,
    [EXECUTE_SEQ] [numeric](2, 0) NULL,
    [View_Access] [numeric](1, 0) NULL,
    [Seeing] [numeric](1, 0) NULL,
 CONSTRAINT [PKBachTskQ] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [Batch_Tasks_QueueData]
) ON [Batch_Tasks_QueueData] TEXTIMAGE_ON [Batch_Tasks_QueueData]
GO    
SET ANSI_PADDING OFF
GO
ALTER TABLE [dbo].[Batch_Tasks_Queue]  WITH NOCHECK ADD  CONSTRAINT [FK0_BtchTskQ_BlngUsr] FOREIGN KEY([Billing_UserId])
REFERENCES [dbo].[BILLING_USER] ([ID])
GO
ALTER TABLE [dbo].[Batch_Tasks_Queue] CHECK CONSTRAINT [FK0_BtchTskQ_BlngUsr]
GO
Hamid Fathi
fonte
A discussão sobre esta questão foi movida para esta sala de bate-papo .
Paul White 9

Respostas:

15

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:

Plano lento

Os recursos interessantes são:

  1. 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.
  2. A Varredura no nó 4 localiza linhas da tabela em que Start_Timenã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.
  3. 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.
  4. 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).
  5. 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.
  6. A Varredura no nó 5 varre completamente a tabela em cada iteração. Ele localiza linhas onde Start_Timenã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.
  7. A junção de loops aninhados no nó 2 também aplica os predicados de junção. Ele verifica se há Operation_Typecorrespondência, se o Start_Timenó 4 é menor que o Start_Timenó 5, se o Start_Timenó 5 é menor que o Finish_Timenó 4 e se os dois Idvalores não correspondem.
  8. 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

Plano menos lento

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:

  1. 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_Typee Start_Time, com Idcomo uma coluna não-chave.
  2. 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 Idda verificação no nó 4 são passados para o ramo do lado interno como referências externas.
  3. A verificação no nó 7 é realizada apenas uma vez.
  4. O spool de índice no nó 6 procura linhas do índice temporário onde Operation_Typecorresponde ao valor de referência externa atual e Start_Timeestá no intervalo definido pelas referências externas Start_Timee Finish_Time.
  5. O Filtro no nó 5 testa os Idvalores 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 Idcomo 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 [Parameterscoluna] é 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 Idcomo 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 FORCESEEKdica 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 é:

Plano de índice filtrado simples

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:

  1. Um prefixo de busca corresponde a Operation_Typee State= 3, buscando a faixa de Start_Timevalores, predicado residual da Iddesigualdade.
  2. Um prefixo de busca corresponde a Operation_Typee State= 4, buscando a faixa de Start_Timevalores, predicado residual da Iddesigualdade.

Onde a [Parameters]coluna é necessária, o plano de consulta simplesmente adiciona no máximo 100 pesquisas singleton para cada tabela:

Plano de índice filtrado com colunas extras

Como nota final, considere usar os tipos inteiros padrão incorporados em vez de numericonde aplicável.

Paul White 9
fonte
-2

Por favor, crie o seguinte índice:

create index Batch_Tasks_Queue_ix_Start_Time on Batch_Tasks_Queue(Start_Time);
David Markovitz
fonte
Tentaste? Fiz alguns testes e fez uma enorme diferença.
David Markovitz