Optimisation des performances sur une requête


9

Demander de l'aide pour améliorer les performances de cette requête.

SQL Server 2008 R2 Enterprise , Max RAM 16 Go, CPU 40, Max Degree of Parallelism 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;

Message d'exécution,

(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.

Structure des tableaux:

-- 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
)

Plan d'exécution:

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


Mettre à jour après avoir obtenu une réponse

Merci beaucoup @Joe Obbish

Vous avez raison sur le problème de cette requête qui concerne environ DsJobStat et DsAvg. Il ne s'agit pas de savoir comment rejoindre et ne pas utiliser NOT IN.

Il y a bien une table comme vous l'avez deviné.

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

J'ai essayé ta suggestion,

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;   

Message d'exécution:

(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.

Plan d'exécution: https://www.brentozar.com/pastetheplan/?id=rJVkLSZ7f


Si vous ne pouvez pas modifier le code du fournisseur, la meilleure chose à faire est d'ouvrir un incident de support avec le fournisseur, aussi douloureux que cela puisse être, et de le battre pour avoir une requête qui nécessite autant de lectures. La clause NOT IN qui fait référence aux valeurs d'une table de 413 000 lignes est, euh, sous-optimale. L'analyse d'index sur DSJobStat retourne 212 millions de lignes, ce qui bouillonne jusqu'à 212 millions de boucles imbriquées, et vous pouvez voir que le nombre de 212 millions de lignes représente 83% du coût. Je ne pense pas que vous puissiez aider cela sans réécrire la requête ou purger les données ...
Tony Hinkle

Je ne comprends pas, comment se fait-il que la suggestion d'Evan ne vous ait pas aidé en premier lieu, les deux réponses sont les mêmes, sauf l'explication.
KumarHarsh

Réponses:


11

Commençons par considérer l'ordre de jointure. Vous avez trois références de table dans la requête. Quel ordre de jointure pourrait vous donner les meilleures performances? L'optimiseur de requêtes pense que la jointure de DsJobStatà DsAvgéliminera presque toutes les lignes (les estimations de cardinalité tombent de 212195000 à 1 ligne). Le plan actuel nous montre que l'estimation est assez proche de la réalité (11 rangées survivent à la jointure). Cependant, la jointure est implémentée comme une jointure anti semi-fusion droite, de sorte que les 212 millions de lignes de la DsJobStattable sont analysées juste pour produire 11 lignes. Cela pourrait certainement contribuer au long temps d'exécution des requêtes, mais je ne peux pas penser à un meilleur opérateur physique ou logique pour cette jointure qui aurait été mieux. Je suis sûr que leDJS_Dashboard_2L'index est utilisé pour d'autres requêtes, mais toutes les clés supplémentaires et les colonnes incluses nécessiteront simplement plus d'E / S pour cette requête et vous ralentiront. Vous avez donc potentiellement un problème d'accès à la table avec l'analyse d'index sur la DsJobStattable.

Je vais supposer que la jointure AJFn'est pas très sélective. Il n'est actuellement pas pertinent pour les problèmes de performances que vous voyez dans la requête, donc je vais l'ignorer pour le reste de cette réponse. Cela pourrait changer si les données du tableau changent.

L'autre problème qui ressort du plan est l'opérateur de spoule de comptage de lignes. Il s'agit d'un opérateur très léger, mais il s'exécute plus de 200 millions de fois. L'opérateur est là car la requête est écrite avec NOT IN. S'il y a une seule ligne NULL, DsAvgtoutes les lignes doivent être supprimées. Le spool est l'implémentation de cette vérification. Ce n'est probablement pas la logique que vous souhaitez, alors vous feriez mieux d'écrire cette partie à utiliser NOT EXISTS. L'avantage réel de cette réécriture dépendra de votre système et de vos données.

J'ai simulé certaines données basées sur le plan de requête pour tester quelques réécritures de requête. Mes définitions de table sont considérablement différentes des vôtres, car il aurait fallu trop d'efforts pour simuler des données pour chaque colonne. Même avec les structures de données abrégées, j'ai pu reproduire le problème de performances que vous rencontrez.

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;

Sur la base du plan de requête, nous pouvons voir qu'il y a environ 200 000 JobNamevaleurs uniques dans le DsAvgtableau. Sur la base du nombre réel de lignes après la jointure à cette table, nous pouvons voir que presque toutes les JobNamevaleurs de DsJobStatsont également dans la DsAvgtable. Ainsi, la DsJobStattable a 200001 valeurs uniques pour la JobNamecolonne et 1000 lignes par valeur.

Je crois que cette requête représente le problème de performances:

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

Toutes les autres choses dans votre plan de requête ( GROUP BY, HAVING, style ancien rejoindre, etc.) se produit après que le jeu de résultats a été réduit à 11 lignes. Actuellement, cela n'a pas d'importance du point de vue des performances des requêtes, mais il pourrait y avoir d'autres problèmes qui pourraient être révélés par des données modifiées dans vos tables.

Je teste dans SQL Server 2017, mais j'ai la même forme de plan de base que vous:

avant le plan

Sur ma machine, cette requête prend 62219 ms de temps CPU et 65576 ms de temps écoulé pour s'exécuter. Si je réécris la requête à utiliser 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);

pas de bobine

Le spool n'est plus exécuté 212 millions de fois et il a probablement le comportement souhaité du fournisseur. Maintenant, la requête s'exécute en 34516 ms de temps CPU et 41132 ms de temps écoulé. La majorité du temps est consacrée à l'analyse de 212 millions de lignes à partir de l'index.

Cette analyse d'index est très regrettable pour cette requête. En moyenne, nous avons 1000 lignes par valeur unique de JobName, mais nous savons après lecture de la première ligne si nous aurons besoin des 1000 lignes précédentes. Nous n'avons presque jamais besoin de ces lignes, mais nous devons quand même les analyser. Si nous savons que les lignes ne sont pas très denses dans la table et que presque toutes seront éliminées par la jointure, nous pouvons imaginer un modèle d'E / S éventuellement plus efficace sur l'index. Que se passe-t-il si SQL Server lit la première ligne par valeur unique de JobName, vérifie si cette valeur est dedans DsAvget passe simplement à la valeur suivante de JobNamesi c'est le cas? Au lieu d'analyser 212 millions de lignes, un plan de recherche nécessitant environ 200 000 exécutions pourrait être effectué à la place.

Cela peut principalement être accompli en utilisant la récursivité avec une technique que Paul White a inventée et qui est décrite ici . Nous pouvons utiliser la récursivité pour faire le modèle d'E / S que j'ai décrit ci-dessus:

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);

Cette requête est beaucoup à examiner, je recommande donc d'examiner attentivement le plan réel . D'abord, nous faisons 200002 index cherche contre l'index DsJobStatpour obtenir toutes les JobNamevaleurs uniques . Ensuite, nous nous joignons à DsAvget éliminons toutes les lignes sauf une. Pour la ligne restante, rejoignez DsJobStatet obtenez toutes les colonnes requises.

Le modèle d'E / S change totalement. Avant d'avoir ceci:

Tableau 'DsJobStat'. Nombre de numérisations 1, lectures logiques 1091651, lectures physiques 13836, lectures anticipées 181966

Avec la requête récursive, nous obtenons ceci:

Tableau 'DsJobStat'. Nombre de scans 200003, lectures logiques 1398000, lectures physiques 1, lectures anticipées 7345

Sur ma machine, la nouvelle requête s'exécute en seulement 6891 ms de temps CPU et 7107 ms de temps écoulé. Notez que la nécessité d'utiliser la récursion de cette manière suggère que quelque chose manque dans le modèle de données (ou peut-être qu'il n'était simplement pas indiqué dans la question publiée). S'il existe une table relativement petite qui contient tout JobNamesce qui est possible, il vaudra beaucoup mieux utiliser cette table que la récursivité sur la grande table. Cela revient à dire que si vous avez un jeu de résultats contenant tout ce JobNamesdont vous avez besoin, vous pouvez utiliser l'index pour obtenir le reste des colonnes manquantes. Cependant, vous ne pouvez pas le faire avec un ensemble de résultats JobNamesdont vous n'avez PAS besoin.


Ai-je suggéré NOT EXISTS. Ils ont déjà répondu par "J'ai déjà essayé les deux, adhérer et n'existe pas, avant de poster une question. Pas beaucoup de différence."
Evan Carroll

1
Je serais curieux de savoir si l'idée récursive fonctionne, c'est terrifiant cependant.
Evan Carroll du

Je pense qu'avoir une clause n'est pas nécessaire. "ElapsedSec n'est pas nul" dans la clause where.For je pense aussi que le CTE récursif n'est pas requis. Qu'avez-vous à dire sur mon idée?
KumarHarsh

@Joe Obbish, j'ai mis à jour mon message. Merci beaucoup.
Wendy

oui, Recursive CTE out effectue row_number () sur (partition par nom de travail, ordre par nom) rn par 1 minute.Mais en même temps, je n'ai vu aucun gain supplémentaire dans Recursive CTE en utilisant vos exemples de données.
KumarHarsh

0

Voyez ce qui se passe si vous réécrivez la condition,

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

À

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

Pensez également à réécrire votre jointure SQL89 car ce style est horrible.

Au lieu de

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

Essayer

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

Je soupçonne également que cette condition peut être mieux écrite, mais nous devons en savoir plus sur ce qui se passe

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

Faut-il vraiment savoir que la moyenne n'est pas nulle, ou simplement qu'un élément du groupe n'est pas nul?


@EvanCarroll. J'ai déjà essayé les deux, rejoindre et n'existe pas, avant de poster une question. Pas beaucoup de différence.
Wendy
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.