Ajuste de desempenho em uma consulta

9

Procurando ajuda para melhorar o desempenho desta consulta.

SQL Server 2008 R2 Enterprise , RAM máxima de 16 GB, CPU 40, Grau máximo de paralelismo 4.

SELECT DsJobStat.JobName AS JobName
    , AJF.ApplGroup AS GroupName
    , DsJobStat.JobStatus AS JobStatus
    , AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) AS ElapsedSecAVG
    , AVG(CAST(DsJobStat.CpuMSec AS FLOAT)) AS CpuMSecAVG 
FROM DsJobStat, AJF 
WHERE DsJobStat.NumericOrderNo=AJF.OrderNo 
AND DsJobStat.Odate=AJF.Odate 
AND DsJobStat.JobName NOT IN( SELECT [DsAvg].JobName FROM [DsAvg] )         
GROUP BY DsJobStat.JobName
, AJF.ApplGroup
, DsJobStat.JobStatus
HAVING AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) <> 0;

Mensagem de execução,

(0 row(s) affected)
Table 'AJF'. Scan count 11, logical reads 45, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DsAvg'. Scan count 2, logical reads 1926, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DsJobStat'. Scan count 1, logical reads 3831235, physical reads 85, read-ahead reads 3724396, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

(1 row(s) affected)

SQL Server Execution Times:
      CPU time = 67268 ms,  elapsed time = 90206 ms.

Estrutura das tabelas:

-- 212271023 rows
CREATE TABLE [dbo].[DsJobStat](
    [OrderID] [nvarchar](8) NOT NULL,
    [JobNo] [int] NOT NULL,
    [Odate] [datetime] NOT NULL,
    [TaskType] [nvarchar](255) NULL,
    [JobName] [nvarchar](255) NOT NULL,
    [StartTime] [datetime] NULL,
    [EndTime] [datetime] NULL,
    [NodeID] [nvarchar](255) NULL,
    [GroupName] [nvarchar](255) NULL,
    [CompStat] [int] NULL,
    [RerunCounter] [int] NOT NULL,
    [JobStatus] [nvarchar](255) NULL,
    [CpuMSec] [int] NULL,
    [ElapsedSec] [int] NULL,
    [StatusReason] [nvarchar](255) NULL,
    [NumericOrderNo] [int] NULL,
CONSTRAINT [PK_DsJobStat] PRIMARY KEY CLUSTERED 
(   [OrderID] ASC,
    [JobNo] ASC,
    [Odate] ASC,
    [JobName] ASC,
    [RerunCounter] ASC
));

-- 48992126 rows
CREATE TABLE [dbo].[AJF](  
    [JobName] [nvarchar](255) NOT NULL,
    [JobNo] [int] NOT NULL,
    [OrderNo] [int] NOT NULL,
    [Odate] [datetime] NOT NULL,
    [SchedTab] [nvarchar](255) NULL,
    [Application] [nvarchar](255) NULL,
    [ApplGroup] [nvarchar](255) NULL,
    [GroupName] [nvarchar](255) NULL,
    [NodeID] [nvarchar](255) NULL,
    [Memlib] [nvarchar](255) NULL,
    [Memname] [nvarchar](255) NULL,
    [CreationTime] [datetime] NULL,
CONSTRAINT [AJF$PrimaryKey] PRIMARY KEY CLUSTERED 
(   [JobName] ASC,
    [JobNo] ASC,
    [OrderNo] ASC,
    [Odate] ASC
));

-- 413176 rows
CREATE TABLE [dbo].[DsAvg](
    [JobName] [nvarchar](255) NULL,
    [GroupName] [nvarchar](255) NULL,
    [JobStatus] [nvarchar](255) NULL,
    [ElapsedSecAVG] [float] NULL,
    [CpuMSecAVG] [float] NULL
);

CREATE NONCLUSTERED INDEX [DJS_Dashboard_2] ON [dbo].[DsJobStat] 
(   [JobName] ASC,
    [Odate] ASC,
    [StartTime] ASC,
    [EndTime] ASC
)
INCLUDE ( [OrderID],
[JobNo],
[NodeID],
[GroupName],
[JobStatus],
[CpuMSec],
[ElapsedSec],
[NumericOrderNo]) ;

CREATE NONCLUSTERED INDEX [Idx_Dashboard_AJF] ON [dbo].[AJF] 
(   [OrderNo] ASC,
[Odate] ASC
)
INCLUDE ( [SchedTab],
[Application],
[ApplGroup]) ;

CREATE NONCLUSTERED INDEX [DsAvg$JobName] ON [dbo].[DsAvg] 
(   [JobName] ASC
)

Plano de execução:

https://www.brentozar.com/pastetheplan/?id=rkUVhMlXM


Atualizar depois de receber respostas

Muito obrigado @Joe Obbish

Você está certo sobre o problema desta consulta, que é entre DsJobStat e DsAvg. Não se trata muito de como participar e não usar NOT IN.

De fato, há uma mesa como você adivinhou.

CREATE TABLE [dbo].[DSJobNames](
    [JobName] [nvarchar](255) NOT NULL,
 CONSTRAINT [DSJobNames$PrimaryKey] PRIMARY KEY CLUSTERED 
(   [JobName] ASC
) ); 

Eu tentei sua sugestão,

SELECT DsJobStat.JobName AS JobName
, AJF.ApplGroup AS GroupName
, DsJobStat.JobStatus AS JobStatus
, AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) AS ElapsedSecAVG
, Avg(CAST(DsJobStat.CpuMSec AS FLOAT)) AS CpuMSecAVG 
FROM DsJobStat
INNER JOIN DSJobNames jn
    ON jn.[JobName]= DsJobStat.[JobName]
INNER JOIN AJF 
    ON DsJobStat.Odate=AJF.Odate 
    AND DsJobStat.NumericOrderNo=AJF.OrderNo 
WHERE NOT EXISTS ( SELECT 1 FROM [DsAvg] WHERE jn.JobName =  [DsAvg].JobName )      
GROUP BY DsJobStat.JobName, AJF.ApplGroup, DsJobStat.JobStatus
HAVING AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) <> 0;   

Mensagem de execução:

(0 row(s) affected)
Table 'DSJobNames'. Scan count 5, logical reads 1244, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DsAvg'. Scan count 5, logical reads 2129, physical reads 0, read-ahead reads 24, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DsJobStat'. Scan count 8, logical reads 84, physical reads 0, read-ahead reads 83, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'AJF'. Scan count 5, logical reads 757999, physical reads 944, read-ahead reads 757311, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

(1 row(s) affected)

 SQL Server Execution Times:
   CPU time = 21776 ms,  elapsed time = 33984 ms.

Plano de execução: https://www.brentozar.com/pastetheplan/?id=rJVkLSZ7f

Wendy
fonte
Se não for possível alterar o código do fornecedor, a melhor coisa a fazer é abrir um incidente de suporte com o fornecedor, por mais doloroso que seja, e eliminá-lo por ter uma consulta que exige que muitas leituras sejam realizadas. A cláusula NOT IN que se refere aos valores em uma tabela com 413 mil linhas é, sub-ideal. A varredura de índice no DSJobStat está retornando 212 milhões de linhas, que produzem até 212 milhões de loops aninhados, e você pode ver que as contagens de 212 milhões de linhas representam 83% do custo. Eu não acho que você pode ajudar isso sem reescrever a consulta ou limpar os dados ...
Tony Hinkle
Eu não entendo, como é que a sugestão de Evan não o ajudou em primeiro lugar, as duas respostas são as mesmas, exceto as explicações. Além disso, não vejo que você tenha implementado completamente o que os dois sugeriram.
KumarHarsh

Respostas:

11

Vamos começar considerando a ordem de junção. Você tem três referências de tabela na consulta. Qual pedido de associação pode oferecer o melhor desempenho? O otimizador de consulta considera que a junção de DsJobStatpara DsAvgeliminará quase todas as linhas (as estimativas de cardinalidade caem de 212195000 para 1 linha). O plano real nos mostra que a estimativa é bem próxima da realidade (11 linhas sobrevivem à junção). No entanto, a junção é implementada como uma junção anti-mesclagem correta, para que todas as 212 milhões de linhas da DsJobStattabela sejam varridas apenas para produzir 11 linhas. Isso certamente poderia estar contribuindo para o longo tempo de execução da consulta, mas não consigo pensar em um operador físico ou lógico melhor para essa associação que teria sido melhor. Tenho certeza de que oDJS_Dashboard_2O índice é usado para outras consultas, mas todas as chaves extras e as colunas incluídas exigirão apenas mais IO para essa consulta e o atrasarão. Portanto, você potencialmente tem um problema de acesso à tabela com a verificação de índice na DsJobStattabela.

Vou assumir que a associação AJFnão é muito seletiva. No momento, ele não é relevante para os problemas de desempenho que você está vendo na consulta, então vou ignorá-lo pelo restante desta resposta. Isso pode mudar se os dados na tabela mudarem.

O outro problema aparente no plano é o operador de spool de contagem de linhas. Este é um operador muito leve, mas está executando mais de 200 milhões de vezes. O operador está lá porque a consulta é gravada com NOT IN. Se houver uma única linha NULL DsAvg, todas as linhas deverão ser eliminadas. O spool é a implementação dessa verificação. Essa provavelmente não é a lógica que você deseja, então seria melhor escrever essa parte para usar NOT EXISTS. O benefício real dessa reescrita dependerá do seu sistema e dados.

Simulei alguns dados com base no plano de consulta para testar algumas reescritas de consulta. Minhas definições de tabela são significativamente diferentes das suas porque seria muito difícil simular dados para cada coluna. Mesmo com as estruturas de dados abreviadas, pude reproduzir o problema de desempenho que você está enfrentando.

CREATE TABLE [dbo].[DsAvg](
    [JobName] [nvarchar](255) NULL
);

CREATE CLUSTERED INDEX CI_DsAvg ON [DsAvg] (JobName);

INSERT INTO [DsAvg] WITH (TABLOCK)
SELECT TOP (200000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

CREATE TABLE [dbo].[DsJobStat](
    [JobName] [nvarchar](255) NOT NULL,
    [JobStatus] [nvarchar](255) NULL,
);

CREATE CLUSTERED INDEX CI_JobStat ON DsJobStat (JobName)

INSERT INTO [DsJobStat] WITH (TABLOCK)
SELECT [JobName], 'ACTIVE'
FROM [DsAvg] ds
CROSS JOIN (
SELECT TOP (1000) 1
FROM master..spt_values t1
) c (t);

INSERT INTO [DsJobStat] WITH (TABLOCK)
SELECT TOP (1000) '200001', 'ACTIVE'
FROM master..spt_values t1;

Com base no plano de consulta, podemos ver que existem cerca de 200000 JobNamevalores exclusivos na DsAvgtabela. Com base no número real de linhas após a junção a essa tabela, podemos ver que quase todos os JobNamevalores DsJobStattambém estão na DsAvgtabela. Portanto, a DsJobStattabela possui 200001 valores exclusivos para a JobNamecoluna e 1000 linhas por valor.

Acredito que esta consulta represente o problema de desempenho:

SELECT DsJobStat.JobName AS JobName, DsJobStat.JobStatus AS JobStatus
FROM DsJobStat
WHERE DsJobStat.JobName NOT IN( SELECT [DsAvg].JobName FROM [DsAvg] );

Todas as outras coisas no seu plano de consulta ( GROUP BY, HAVINGjunção de estilo antigo etc.) acontecem depois que o conjunto de resultados é reduzido para 11 linhas. Atualmente, não importa do ponto de vista do desempenho da consulta, mas pode haver outras preocupações que podem ser reveladas pelos dados alterados em suas tabelas.

Estou testando no SQL Server 2017, mas recebo a mesma forma básica de plano que você:

antes do plano

Na minha máquina, essa consulta leva 62219 ms de tempo da CPU e 65576 ms de tempo decorrido para executar. Se eu reescrever a consulta para usar NOT EXISTS:

SELECT DsJobStat.JobName AS JobName, DsJobStat.JobStatus AS JobStatus
FROM DsJobStat
WHERE NOT EXISTS (SELECT 1 FROM [DsAvg] WHERE DsJobStat.JobName = [DsAvg].JobName);

sem carretel

O spool não é mais executado 212 milhões de vezes e provavelmente tem o comportamento pretendido pelo fornecedor. Agora, a consulta é executada em 34516 ms do tempo da CPU e 41132 ms do tempo decorrido. A maior parte do tempo é gasta na digitalização de 212 milhões de linhas a partir do índice.

Essa verificação de índice é muito infeliz para essa consulta. Em média, temos 1000 linhas por valor único de JobName, mas sabemos depois de ler a primeira linha se precisaremos das 1000 linhas anteriores. Quase nunca precisamos dessas linhas, mas ainda precisamos varrê-las de qualquer maneira. Se soubermos que as linhas não são muito densas na tabela e que quase todas serão eliminadas pela junção, podemos imaginar um padrão de IO possivelmente mais eficiente no índice. E se o SQL Server leu a primeira linha por valor exclusivo de JobName, verificou se esse valor estava DsAvge simplesmente passou para o próximo valor de JobNamese estivesse? Em vez de digitalizar 212 milhões de linhas, um plano de busca que requer cerca de 200 mil execuções pode ser feito.

Isso pode ser feito principalmente usando recursão junto com uma técnica pioneira de Paul White descrita aqui . Podemos usar a recursão para executar o padrão de E / S que descrevi acima:

WITH RecursiveCTE
AS
(
    -- Anchor
    SELECT TOP (1)
        [JobName]
    FROM dbo.DsJobStat AS T
    ORDER BY
        T.[JobName]

    UNION ALL

    -- Recursive
    SELECT R.[JobName]
    FROM
    (
        -- Number the rows
        SELECT 
            T.[JobName],
            rn = ROW_NUMBER() OVER (
                ORDER BY T.[JobName])
        FROM dbo.DsJobStat AS T
        JOIN RecursiveCTE AS R
            ON R.[JobName] < T.[JobName]
    ) AS R
    WHERE
        -- Only the row that sorts lowest
        R.rn = 1
)
SELECT js.*
FROM RecursiveCTE
INNER JOIN dbo.DsJobStat js ON RecursiveCTE.[JobName]= js.[JobName]
WHERE NOT EXISTS (SELECT 1 FROM [DsAvg] WHERE RecursiveCTE.JobName = [DsAvg].JobName)
OPTION (MAXRECURSION 0);

Essa consulta é muito difícil de analisar, por isso recomendo examinar cuidadosamente o plano real . Primeiro, fazemos 200002 buscas no índice DsJobStatpara obter todos os JobNamevalores exclusivos . Em seguida, ingressamos DsAvge eliminamos todas as linhas, exceto uma. Para a linha restante, retorne DsJobState obtenha todas as colunas necessárias.

O padrão de IO muda totalmente. Antes de conseguirmos isso:

Tabela 'DsJobStat'. Contagem de varredura 1, leituras lógicas 1091651, leituras físicas 13836, leituras antecipadas 181966

Com a consulta recursiva, obtemos o seguinte:

Tabela 'DsJobStat'. Contagem de varreduras 200003, leituras lógicas 1398000, leituras físicas 1, leituras antecipadas 7345

Na minha máquina, a nova consulta é executada em apenas 6891 ms de tempo de CPU e 7107 ms de tempo decorrido. Observe que a necessidade de usar a recursão dessa maneira sugere que algo está faltando no modelo de dados (ou talvez não tenha sido declarado na pergunta postada). Se houver uma tabela relativamente pequena que contenha todo o possível JobNames, será muito melhor usá-la em oposição à recursão na mesa grande. O que se resume a isso é que, se você tiver um conjunto de resultados contendo tudo o JobNamesque precisa, poderá usar o índice procura obter o restante das colunas ausentes. No entanto, você não pode fazer isso com um conjunto de resultados do JobNamesque NÃO precisa.

Joe Obbish
fonte
Eu sugeri NOT EXISTS. Eles já responderam com "Eu já tentei os dois, entre e não existe, antes de postar a pergunta. Não há muita diferença."
Evan Carroll
11
Eu ficaria curioso para saber se a ideia recursiva funciona, mas isso é aterrorizante.
Evan Carroll
Eu acho que ter cláusula não é necessária. "ElapsedSec não é nulo" na cláusula where. Também acho que CTE recursiva não é necessária. você pode usar row_number () over (partição por ordem de nome do trabalho por nome) rn onde não existe (selecione consulta). o que você tem a dizer sobre a minha ideia?
KumarHarsh
@ Joe Obbish, eu atualizei meu post. Muito obrigado.
Wendy
Sim, o CTE recursivo executa o row_number () over (partição por ordem do nome do trabalho por nome) rn por 1 minuto.
KumarHarsh
0

Veja o que acontece se você reescrever a condição,

AND DsJobStat.JobName NOT IN( SELECT [DsAvg].JobName FROM [DsAvg] )         

Para

AND NOT EXISTS ( SELECT 1 FROM [DsAvg] AS d WHERE d.JobName = DsJobStat.JobName )

Considere também reescrever sua junção SQL89 porque esse estilo é horrível.

Ao invés de

FROM DsJobStat, AJF 
WHERE DsJobStat.NumericOrderNo=AJF.OrderNo 
AND DsJobStat.Odate=AJF.Odate 

Tentar

FROM DsJobStat
INNER JOIN AJF ON (
  DsJobStat.NumericOrderNo=AJF.OrderNo 
  AND DsJobStat.Odate=AJF.Odate
)

Eu também suspeito que essa condição possa ser escrita melhor, mas teríamos que saber mais sobre o que está acontecendo

HAVING AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) <> 0;

Você realmente precisa saber que a média não é zero ou apenas que um elemento do grupo não é zero?

Evan Carroll
fonte
@EvanCarroll. Eu já tentei os dois, entre e não existe, antes de postar a pergunta. Pouca diferença.
Wendy