Leistungsoptimierung für eine Abfrage

9

Ich suche Hilfe, um diese Abfrageleistung zu verbessern.

SQL Server 2008 R2 Enterprise , maximaler RAM 16 GB, CPU 40, maximaler Parallelitätsgrad 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;

Ausführungsnachricht,

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

Tabellenstruktur:

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

Ausführungsplan:

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


Update nach Beantwortung

Vielen Dank @Joe Obbish

Sie haben Recht mit dem Problem dieser Abfrage, die zwischen DsJobStat und DsAvg auftritt. Es geht nicht viel darum, sich anzumelden und NOT IN nicht zu verwenden.

Es gibt tatsächlich einen Tisch, wie Sie vermutet haben.

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

Ich habe Ihren Vorschlag ausprobiert,

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;   

Ausführungsnachricht:

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

Ausführungsplan: https://www.brentozar.com/pastetheplan/?id=rJVkLSZ7f

Wendy
quelle
Wenn es sich um einen Lieferantencode handelt, den Sie nicht ändern können, ist es am besten, einen Support-Vorfall mit dem Lieferanten zu eröffnen, so schmerzhaft das auch sein mag, und ihn für eine Abfrage zu verprügeln, für deren Erfüllung so viele Lesevorgänge erforderlich sind. Die NOT IN-Klausel, die sich auf Werte in einer Tabelle mit 413.000 Zeilen bezieht, ist nicht optimal. Der Index-Scan auf DSJobStat gibt 212 Millionen Zeilen zurück, wodurch bis zu 212 Millionen verschachtelte Schleifen entstehen, und Sie können sehen, dass die Anzahl der 212 Millionen Zeilen 83% der Kosten beträgt. Ich glaube nicht, dass Sie dies ändern können, ohne die Abfrage neu zu schreiben oder Daten zu löschen ...
Tony Hinkle
Ich verstehe nicht, warum Evan Vorschlag dir überhaupt nicht geholfen hat, beide Antworten sind bis auf die Erklärung gleich. Ich sehe auch nicht, dass du das, was diese beiden Jungs dir vorgeschlagen haben, vollständig umgesetzt hast. Joe hat diese Frage interessant gemacht.
KumarHarsh

Antworten:

11

Beginnen wir mit der Betrachtung der Beitrittsreihenfolge. Sie haben drei Tabellenreferenzen in der Abfrage. Welche Beitrittsreihenfolge bietet Ihnen möglicherweise die beste Leistung? Der Abfrageoptimierer geht davon aus, dass durch die Verknüpfung von DsJobStatbis DsAvgfast alle Zeilen entfernt werden (Kardinalitätsschätzungen fallen von 212195000 auf 1 Zeile). Der tatsächliche Plan zeigt uns, dass die Schätzung der Realität ziemlich nahe kommt (11 Zeilen überleben den Join). Der Join wird jedoch als rechter Anti-Semi-Merge-Join implementiert, sodass alle 212 Millionen Zeilen aus der DsJobStatTabelle gescannt werden, um nur 11 Zeilen zu erzeugen. Das könnte sicherlich zur langen Ausführungszeit der Abfrage beitragen, aber ich kann mir keinen besseren physischen oder logischen Operator für diesen Join vorstellen, der besser gewesen wäre. Ich bin mir sicher, dass dieDJS_Dashboard_2Der Index wird für andere Abfragen verwendet, aber alle zusätzlichen Schlüssel und enthaltenen Spalten erfordern nur mehr E / A für diese Abfrage und verlangsamen Sie. Sie haben also möglicherweise ein Tabellenzugriffsproblem mit dem Index-Scan für die DsJobStatTabelle.

Ich gehe davon aus, dass der Beitritt zu AJFnicht sehr selektiv ist. Es ist derzeit nicht relevant für die Leistungsprobleme, die in der Abfrage angezeigt werden. Daher werde ich es für den Rest dieser Antwort ignorieren. Das könnte sich ändern, wenn sich die Daten in der Tabelle ändern.

Das andere Problem, das aus dem Plan hervorgeht, ist der Zeilenanzahl-Spool-Operator. Dies ist ein sehr leichter Bediener, der jedoch über 200 Millionen Mal ausgeführt wird. Der Operator ist da, weil die Abfrage mit geschrieben wurde NOT IN. Wenn es eine einzelne NULL-Zeile gibt, DsAvgmüssen alle Zeilen entfernt werden. Die Spool ist die Implementierung dieser Prüfung. Das ist wahrscheinlich nicht die Logik, die Sie wollen, also sollten Sie diesen Teil besser schreiben, um ihn zu verwenden NOT EXISTS. Der tatsächliche Nutzen dieses Umschreibens hängt von Ihrem System und Ihren Daten ab.

Ich habe einige Daten basierend auf dem Abfrageplan verspottet, um einige Umschreibungen von Abfragen zu testen. Meine Tabellendefinitionen unterscheiden sich erheblich von Ihren, da es zu aufwändig gewesen wäre, Daten für jede einzelne Spalte zu verspotten. Selbst mit den abgekürzten Datenstrukturen konnte ich das aufgetretene Leistungsproblem reproduzieren.

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;

Anhand des Abfrageplans können wir feststellen, dass JobNamedie DsAvgTabelle etwa 200000 eindeutige Werte enthält . Anhand der tatsächlichen Anzahl der Zeilen nach dem Join zu dieser Tabelle können wir sehen, dass fast alle JobNameWerte in DsJobStatauch in der DsAvgTabelle enthalten sind. Somit enthält die DsJobStatTabelle 200001 eindeutige Werte für die JobNameSpalte und 1000 Zeilen pro Wert.

Ich glaube, dass diese Abfrage das Leistungsproblem darstellt:

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

Alle anderen Sachen in der Abfrage - Plan ( GROUP BY, HAVING, alten Stil verbinden, usw.) geschieht , nachdem die Ergebnismenge auf 11 Zeilen reduziert. Unter dem Gesichtspunkt der Abfrageleistung spielt dies derzeit keine Rolle, es können jedoch auch andere Bedenken auftreten, die durch geänderte Daten in Ihren Tabellen aufgedeckt werden können.

Ich teste in SQL Server 2017, erhalte jedoch die gleiche Grundplanform wie Sie:

vor dem Plan

Auf meinem Computer benötigt diese Abfrage 62219 ms CPU-Zeit und 65576 ms verstrichene Zeit, um ausgeführt zu werden. Wenn ich die zu verwendende Abfrage neu schreibe 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);

keine Spule

Die Spool wird nicht mehr 212 Millionen Mal ausgeführt und hat wahrscheinlich das vom Hersteller beabsichtigte Verhalten. Jetzt wird die Abfrage in 34516 ms CPU-Zeit und 41132 ms verstrichener Zeit ausgeführt. Die meiste Zeit wird damit verbracht, 212 Millionen Zeilen aus dem Index zu scannen.

Dieser Index-Scan ist für diese Abfrage sehr unglücklich. Im Durchschnitt haben wir 1000 Zeilen pro eindeutigem Wert von JobName, aber wir wissen nach dem Lesen der ersten Zeile, ob wir die vorhergehenden 1000 Zeilen benötigen. Wir brauchen diese Zeilen fast nie, aber wir müssen sie trotzdem scannen. Wenn wir wissen, dass die Zeilen in der Tabelle nicht sehr dicht sind und dass fast alle durch den Join eliminiert werden, können wir uns ein möglicherweise effizienteres E / A-Muster im Index vorstellen. Was passiert, wenn SQL Server die erste Zeile pro eindeutigem Wert von liest JobName, prüft, ob dieser Wert vorhanden ist DsAvg, und einfach zum nächsten Wert überspringt, JobNamewenn dies der Fall ist? Anstatt 212 Millionen Zeilen zu scannen, könnte stattdessen ein Suchplan erstellt werden, der etwa 200.000 Ausführungen erfordert.

Dies kann meistens durch Rekursion zusammen mit einer von Paul White entwickelten Technik erreicht werden, die hier beschrieben wird . Wir können die Rekursion verwenden, um das oben beschriebene E / A-Muster zu erstellen:

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

Diese Abfrage ist sehr wichtig, daher empfehle ich, den tatsächlichen Plan sorgfältig zu prüfen . Zuerst führen wir 200002 Indexsuchen gegen den Index durch DsJobStat, um alle eindeutigen JobNameWerte zu erhalten. Dann verbinden wir DsAvgalle Zeilen bis auf eine. Schließen Sie sich für die verbleibende Zeile wieder an DsJobStatund rufen Sie alle erforderlichen Spalten ab.

Das E / A-Muster ändert sich vollständig. Bevor wir das bekamen:

Tabelle 'DsJobStat'. Scananzahl 1, logische Lesevorgänge 1091651, physische Lesevorgänge 13836, Vorlesevorgänge 181966

Mit der rekursiven Abfrage erhalten wir Folgendes:

Tabelle 'DsJobStat'. Scananzahl 200003, logische Lesevorgänge 1398000, physische Lesevorgänge 1, Vorlesevorgänge 7345

Auf meinem Computer wird die neue Abfrage in nur 6891 ms CPU-Zeit und 7107 ms verstrichener Zeit ausgeführt. Beachten Sie, dass die Verwendung der Rekursion auf diese Weise darauf hindeutet, dass im Datenmodell etwas fehlt (oder dass es in der geposteten Frage einfach nicht angegeben wurde). Wenn es eine relativ kleine Tabelle gibt, die alles Mögliche enthält, ist JobNameses viel besser, diese Tabelle zu verwenden, als eine Rekursion auf der großen Tabelle. Wenn Sie eine Ergebnismenge haben, die alles enthält JobNames, was Sie benötigen, können Sie Indexsuchen verwenden, um den Rest der fehlenden Spalten abzurufen. Mit einer Ergebnismenge von können Sie dies jedoch nicht tunJobNames die Sie NICHT benötigen.

Joe Obbish
quelle
Ich schlug vor NOT EXISTS. Sie antworteten bereits mit "Ich habe bereits beide ausprobiert, bin beigetreten und nicht vorhanden, bevor ich eine Frage gestellt habe. Nicht viel Unterschied."
Evan Carroll
1
Ich wäre gespannt, ob die rekursive Idee funktioniert, aber das ist erschreckend.
Evan Carroll
Ich denke, Klausel ist nicht erforderlich. "ElapsedSec ist nicht null" in wo Klausel wird. Auch ich denke, rekursiver CTE ist nicht erforderlich. Sie können row_number () over (Partition nach Jobname Reihenfolge nach Name) rn verwenden, wo nicht vorhanden (auswählen frage) .was hast du zu meiner idee zu sagen?
KumarHarsh
@ Joe Obbish, ich habe meinen Beitrag aktualisiert. Vielen Dank.
Wendy
Ja, rekursiver CTE-Ausgang führt row_number () um (Partition nach Jobname, Reihenfolge nach Name) rn um 1 Minute aus. Gleichzeitig habe ich bei Verwendung Ihrer Beispieldaten keinen zusätzlichen Gewinn bei rekursivem CTE festgestellt.
KumarHarsh
0

Sehen Sie, was passiert, wenn Sie die Bedingung neu schreiben.

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

Zu

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

Überlegen Sie auch, Ihren SQL89-Join neu zu schreiben, da dieser Stil schrecklich ist.

Anstatt

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

Versuchen

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

Ich vermute auch, dass diese Bedingung besser geschrieben werden kann, aber wir müssten mehr darüber wissen, was passiert

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

Müssen Sie wirklich wissen, dass der Durchschnitt nicht Null ist oder dass nur ein Element der Gruppe nicht Null ist?

Evan Carroll
quelle
@EvanCarroll. Ich habe bereits beide ausprobiert, bin beigetreten und nicht vorhanden, bevor ich eine Frage gestellt habe. Kein großer Unterschied.
Wendy