Ottimizzazione delle prestazioni su una query


9

Cerco aiuto per migliorare le prestazioni di questa query.

SQL Server 2008 R2 Enterprise , RAM massima 16 GB, CPU 40, grado massimo di parallelismo 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;

Messaggio di esecuzione,

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

Struttura delle tabelle:

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

Progetto esecutivo:

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


Aggiorna dopo aver ricevuto risposta

Grazie mille @Joe Obbish

Hai ragione sul problema di questa query che riguarda DsJobStat e DsAvg. Non è molto su come ISCRIVERSI e non utilizzare NOT IN.

C'è davvero un tavolo come hai indovinato.

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

Ho provato il tuo suggerimento,

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;   

Messaggio di esecuzione:

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

Piano di esecuzione: https://www.brentozar.com/pastetheplan/?id=rJVkLSZ7f


Se non è possibile modificare il codice del fornitore, la cosa migliore da fare è aprire un incidente di supporto con il fornitore, per quanto doloroso possa essere, e batterlo per avere una query che richiede che molte letture vengano soddisfatte. La clausola NOT IN che fa riferimento a valori in una tabella con 413 mila righe è, in fondo, non ottimale. La scansione dell'indice su DSJobStat sta restituendo 212 milioni di righe, il che bolle fino a 212 milioni di cicli nidificati e puoi vedere che i conteggi di 212 milioni di righe rappresentano l'83% del costo. Non credo che tu possa aiutarlo senza riscrivere la query o eliminare i dati ...
Tony Hinkle,

Non capisco, come mai il suggerimento di Evan non ti abbia aiutato in primo luogo, entrambe le risposte sono le stesse tranne la spiegazione.Inoltre non vedo che hai implementato completamente ciò che entrambi questi ragazzi ti hanno suggerito.Joe ha reso interessante questa domanda.
KumarHarsh,

Risposte:


11

Cominciamo considerando l'ordine di iscrizione. Hai tre riferimenti di tabella nella query. Quale ordine di join potrebbe offrirti le migliori prestazioni? Query Optimizer ritiene che il join da DsJobStata DsAvgeliminerà quasi tutte le righe (le stime di cardinalità scendono da 212195000 a 1 riga). Il piano reale ci mostra che la stima è abbastanza vicina alla realtà (11 righe sopravvivono al join). Tuttavia, il join viene implementato come join anti semi merge corretto, quindi tutti i 212 milioni di righe dalla DsJobStattabella vengono scansionati solo per produrre 11 righe. Ciò potrebbe sicuramente contribuire al lungo tempo di esecuzione della query, ma non riesco a pensare a un operatore fisico o logico migliore per quell'unione che sarebbe stato migliore. Sono sicuro che ilDJS_Dashboard_2L'indice viene utilizzato per altre query, ma tutta la chiave aggiuntiva e le colonne incluse richiederanno solo più IO per questa query e rallenteranno. Quindi potenzialmente hai un problema di accesso alla tabella con la scansione dell'indice sulla DsJobStattabella.

Presumo che l'adesione a AJFnon sia molto selettiva. Al momento non è rilevante per i problemi di prestazioni che stai riscontrando nella query, quindi lo ignorerò per il resto di questa risposta. Ciò potrebbe cambiare se i dati nella tabella cambiassero.

L'altro problema che emerge dal piano è l'operatore di spool conteggio righe. Questo è un operatore molto leggero ma esegue oltre 200 milioni di volte. L'operatore è presente perché la query è scritta con NOT IN. Se è presente una singola riga NULL, DsAvgtutte le righe devono essere eliminate. Lo spool è l'implementazione di quel controllo. Probabilmente questa non è la logica che desideri, quindi faresti meglio a scrivere quella parte da usare NOT EXISTS. Il vantaggio effettivo di tale riscrittura dipenderà dal sistema e dai dati.

Ho preso in giro alcuni dati basati sul piano di query per testare alcune riscritture di query. Le definizioni delle mie tabelle sono significativamente diverse dalle tue perché sarebbe stato troppo difficile simulare i dati per ogni singola colonna. Anche con le strutture di dati abbreviate sono stato in grado di riprodurre il problema di prestazioni che stai riscontrando.

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;

Sulla base del piano di query, possiamo vedere che ci sono circa 200000 JobNamevalori univoci nella DsAvgtabella. In base al numero effettivo di righe dopo l'unione a quella tabella, possiamo vedere che quasi tutti i JobNamevalori in DsJobStatsono anche nella DsAvgtabella. Pertanto, la DsJobStattabella ha 200001 valori univoci per la JobNamecolonna e 1000 righe per valore.

Credo che questa query rappresenti il ​​problema delle prestazioni:

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

Tutte le altre cose nel vostro piano di query ( GROUP BY, HAVING, stile antico si uniscono, ecc) avviene dopo che il set di risultati è stata ridotta a 11 righe. Al momento non importa dal punto di vista delle prestazioni della query, ma potrebbero esserci altre preoccupazioni che potrebbero essere rivelate da dati modificati nelle tabelle.

Sto testando in SQL Server 2017, ma ho la stessa forma del piano di base:

prima del piano

Sulla mia macchina, quella query richiede 62219 ms di tempo CPU e 65576 ms di tempo trascorso per l'esecuzione. Se riscrivo la query da utilizzare 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);

nessuna bobina

La bobina non viene più eseguita 212 milioni di volte e probabilmente ha il comportamento previsto dal fornitore. Ora la query viene eseguita in 34516 ms di tempo CPU e 41132 ms di tempo trascorso. La maggior parte del tempo è dedicato alla scansione di 212 milioni di righe dall'indice.

Quella scansione dell'indice è molto sfortunata per quella query. In media abbiamo 1000 righe per valore univoco di JobName, ma sappiamo dopo aver letto la prima riga se avremo bisogno delle precedenti 1000 righe. Non abbiamo quasi mai bisogno di quelle righe, ma dobbiamo comunque scansionarle comunque. Se sappiamo che le righe non sono molto dense nella tabella e che quasi tutte verranno eliminate dall'unione, possiamo immaginare un modello IO forse più efficiente sull'indice. Cosa succede se SQL Server legge la prima riga per valore univoco di JobName, controlla se quel valore era presente DsAvge passa semplicemente al valore successivo di JobNamese fosse? Invece di scansionare 212 milioni di righe, è possibile invece eseguire un piano di ricerca che richiede circa 200.000 esecuzioni.

Ciò può essere realizzato principalmente utilizzando la ricorsione insieme a una tecnica che Paul White ha aperto la strada qui descritta . Possiamo usare la ricorsione per fare il modello IO che ho descritto sopra:

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

Quella query è molto da guardare, quindi consiglio di esaminare attentamente il piano reale . Innanzitutto eseguiamo 200002 ricerche dell'indice sull'indice DsJobStatper ottenere tutti i JobNamevalori univoci . Quindi ci uniamo DsAvged eliminiamo tutte le righe tranne una. Per la riga rimanente, unisciti di nuovo DsJobState ottieni tutte le colonne richieste.

Il modello IO cambia totalmente. Prima di ottenere questo:

Tabella 'DsJobStat'. Conteggio scansioni 1, letture logiche 1091651, letture fisiche 13836, letture avanti 181966

Con la query ricorsiva otteniamo questo:

Tabella 'DsJobStat'. Conteggio scansioni 200003, letture logiche 1398000, letture fisiche 1, letture avanti 7345

Sulla mia macchina, la nuova query viene eseguita in soli 6891 ms di tempo CPU e 7107 ms di tempo trascorso. Si noti che la necessità di utilizzare la ricorsione in questo modo suggerisce che manca qualcosa nel modello di dati (o forse era semplicemente non dichiarato nella domanda pubblicata). Se esiste una tabella relativamente piccola che contiene tutto il possibile JobNames, sarà molto meglio usare quella tabella invece della ricorsione sul tavolo grande. Ciò a cui si riduce è se si dispone di un set di risultati che contiene tutto ciò di JobNamescui si ha bisogno, quindi è possibile utilizzare ricerche di indice per ottenere il resto delle colonne mancanti. Tuttavia, non puoi farlo con un set di risultati di JobNamescui NON hai bisogno.


Ho suggerito NOT EXISTS. Hanno già risposto con "Ho già provato entrambi, unisciti e non esiste, prima di pubblicare la domanda. Non molta differenza".
Evan Carroll,

1
Sarei curioso di sapere se l'idea ricorsiva funziona, ma è terrificante.
Evan Carroll,

penso che non sia richiesta la clausola. "ElapsedSec non è null" nel punto in cui la clausola farà. Inoltre penso che CTE ricorsivo non sia richiesto. È possibile utilizzare row_number () su (partizione per nome lavoro in ordine di nome) rn dove non esiste (selezionare domanda). cosa hai da dire sulla mia idea?
KumarHarsh,

@Joe Obbish, ho aggiornato il mio post. Molte grazie.
Wendy,

Sì, CTE ricorsivo esegue row_number () over (partizione per nome lavoro ordina per nome) rn di 1 minuto. Allo stesso tempo, non ho visto alcun guadagno aggiuntivo in CTE ricorsivo usando i tuoi dati di esempio.
KumarHarsh,

0

Vedi cosa succede se riscrivi la condizione,

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

Per

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

Considera anche di riscrivere il tuo join SQL89 perché quello stile è orribile.

Invece di

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

Provare

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

Ho anche il sospetto che questa condizione possa essere scritta meglio, ma dovremmo sapere di più su ciò che sta accadendo

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

Devi davvero sapere che la media non è zero o che solo un elemento del gruppo non è zero?


@EvanCarroll. Ho già provato entrambi, unisciti e non esiste, prima di pubblicare la domanda. Non molta differenza.
Wendy, il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.