Buscando ajuda para melhorar o desempenho desta consulta.
SQL Server 2008 R2 Enterprise , Max RAM 16 GB, CPU 40, Max Grau 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 após ser respondido
Muito obrigado @Joe Obbish
Você está certo sobre a questão desta consulta que é entre DsJobStat e DsAvg. Não é muito sobre como JOIN e não usar NOT IN.
Há de fato 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
Vamos começar considerando a ordem de junção. Você tem três referências de tabela na consulta. Qual ordem de junção pode oferecer o melhor desempenho? O otimizador de consulta pensa que a junção de
DsJobStat
toDsAvg
eliminará quase todas as linhas (as estimativas de cardinalidade caem de 212195000 para 1 linha). O plano real nos mostra que a estimativa está bem próxima da realidade (11 linhas sobrevivem à junção). No entanto, a junção é implementada como uma junção anti semi-mesclagem direita, portanto, todas as 212 milhões de linhas daDsJobStat
tabela são verificadas 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 junção que teria sido melhor. tenho certeza que oDJS_Dashboard_2
index é usado para outras consultas, mas todas as chaves extras e colunas incluídas apenas exigirão mais IO para essa consulta e o deixarão mais lento. Portanto, você potencialmente tem um problema de acesso à tabela com a varredura de índice naDsJobStat
tabela.Eu vou assumir que a junção a
AJF
nã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 no restante desta resposta. Isso pode mudar se os dados na tabela forem alterados.O outro problema aparente no plano é o operador de spool de contagem de linhas. Este é um operador muito leve, mas está sendo executado mais de 200 milhões de vezes. O operador está lá porque a consulta é escrita com
NOT IN
. Se houver uma única linha NULLDsAvg
, todas as linhas devem ser eliminadas. O carretel é a implementação dessa verificação. Essa provavelmente não é a lógica que você deseja, então seria melhor escrever essa parte para usarNOT EXISTS
. O benefício real dessa reescrita dependerá do sistema e dos dados.Eu zombei de 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 teria sido muito trabalhoso simular dados para cada coluna. Mesmo com as estruturas de dados abreviadas, consegui reproduzir o problema de desempenho que você está enfrentando.
Com base no plano de consulta, podemos ver que existem cerca de 200.000
JobName
valores únicos naDsAvg
tabela. Com base no número real de linhas após a junção a essa tabela, podemos ver que quase todos osJobName
valores deDsJobStat
também estão naDsAvg
tabela. Assim, aDsJobStat
tabela possui 200.001 valores únicos para aJobName
coluna e 1.000 linhas por valor.Acredito que esta consulta representa o problema de desempenho:
Todas as outras coisas em seu plano de consulta (
GROUP BY
,HAVING
, junção de estilo antigo, etc) acontecem depois que o conjunto de resultados foi 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 por dados alterados em suas tabelas.Estou testando no SQL Server 2017, mas recebo a mesma forma de plano básico que você:
Na minha máquina, essa consulta leva 62.219 ms de tempo de CPU e 65.576 ms de tempo decorrido para ser executada. Se eu reescrever a consulta para usar
NOT EXISTS
:O spool não é mais executado 212 milhões de vezes e provavelmente tem o comportamento pretendido do fornecedor. Agora a consulta é executada em 34.516 ms de tempo de CPU e 41.132 ms de tempo decorrido. A maior parte do tempo é gasto verificando 212 milhões de linhas do índice.
Essa verificação de índice é muito infeliz para essa consulta. Em média, temos 1.000 linhas por valor único de
JobName
, mas sabemos depois de ler a primeira linha se precisaremos das 1.000 linhas anteriores. Quase nunca precisamos dessas linhas, mas ainda precisamos digitalizá-las de qualquer maneira. Se soubermos que as linhas não são muito densas na tabela e que quase todas serão eliminadas pelo join podemos imaginar um padrão de IO possivelmente mais eficiente no índice. E se o SQL Server lesse a primeira linha por valor exclusivo deJobName
, verificasse se esse valor estava emDsAvg
e simplesmente pulasse para o próximo valor deJobName
se estivesse? Em vez de verificar 212 milhões de linhas, um plano de busca exigindo cerca de 200 mil execuções poderia ser feito.Isso pode ser feito principalmente usando recursão junto com uma técnica pioneira de Paul White que é descrita aqui . Podemos usar a recursão para fazer o padrão IO que descrevi acima:
Essa consulta é muito para olhar, então eu recomendo examinar cuidadosamente o plano real . Primeiro, fazemos buscas de índice 200002 contra o índice
DsJobStat
para obter todos osJobName
valores exclusivos. Em seguida, juntamosDsAvg
e eliminamos todas as linhas, exceto uma. Para a linha restante, junte-se novamenteDsJobStat
e obtenha todas as colunas necessárias.O padrão IO muda totalmente. Antes de termos isso:
Com a consulta recursiva, obtemos isso:
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 todas as possibilidades
JobNames
, será muito melhor usar essa tabela em oposição à recursão na tabela grande. O que se resume a isso é que, se você tiver um conjunto de resultados contendo tudo oJobNames
que precisa, poderá usar o índice procura para obter o restante das colunas ausentes. No entanto, você não pode fazer isso com um conjunto de resultadosJobNames
que você NÃO precisa.Veja o que acontece se você reescrever a condição,
Para
Considere também reescrever sua junção SQL89 porque esse estilo é horrível.
Ao invés de
Tentar
Eu também suspeito que essa condição pode ser escrita melhor, mas teríamos que saber mais sobre o que está acontecendo
Você realmente precisa saber que a média não é zero, ou apenas que um elemento do grupo não é zero?