Ottimizza la sottoquery con la funzione Windowing


8

Dato che le mie capacità di ottimizzazione delle prestazioni non sembrano mai sufficienti, mi chiedo sempre se c'è più ottimizzazione che posso eseguire su alcune query. La situazione alla quale si riferisce questa domanda è una funzione Windowed MAX nidificata in una sottoquery.

I dati che sto scavando sono una serie di transazioni su vari gruppi di insiemi più grandi. Ho 4 campi di importanza, l'ID univoco di una transazione, l'ID gruppo di un batch di transazioni e le date associate alla rispettiva transazione univoca o gruppo di transazioni. La maggior parte delle volte la Data di gruppo corrisponde alla Data di transazione unica massima per un batch, ma ci sono momenti in cui le rettifiche manuali arrivano attraverso il nostro sistema e un'operazione di data unica si verifica dopo l'acquisizione della data della transazione di gruppo. Questa modifica manuale non regola la data del gruppo in base alla progettazione.

Ciò che identifico in questa query sono quei record in cui la Data unica cade dopo la Data del gruppo. La seguente query di esempio crea un equivalente approssimativo del mio scenario e l'istruzione SELECT restituisce i record che sto cercando, tuttavia, mi sto avvicinando a questa soluzione nel modo più efficiente? Questo richiede un po 'di tempo per essere eseguito durante il caricamento della tabella dei fatti poiché il mio record conta il numero nelle prime 9 cifre, ma soprattutto il mio disprezzo per le subquery mi fa chiedere se c'è un approccio migliore qui. Non sono preoccupato per gli indici in quanto sono sicuro che siano già in atto; quello che sto cercando è un approccio di query alternativo che raggiungerà la stessa cosa, ma anche in modo più efficiente. Qualsiasi feedback è il benvenuto.

CREATE TABLE #Example
(
    UniqueID INT IDENTITY(1,1)
  , GroupID INT
  , GroupDate DATETIME
  , UniqueDate DATETIME
)

CREATE CLUSTERED INDEX [CX_1] ON [#Example]
(
    [UniqueID] ASC
)


SET NOCOUNT ON

--Populate some test data
DECLARE @i INT = 0, @j INT = 5, @UniqueDate DATETIME, @GroupDate DATETIME

WHILE @i < 10000
BEGIN

    IF((@i + @j)%173 = 0)
    BEGIN
        SET @UniqueDate = GETDATE()+@i+5
    END
    ELSE
    BEGIN
        SET @UniqueDate = GETDATE()+@i
    END

    SET @GroupDate = GETDATE()+(@j-1)

    INSERT INTO #Example (GroupID, GroupDate, UniqueDate)
    VALUES (@j, @GroupDate, @UniqueDate)

    SET @i = @i + 1

    IF (@i % 5 = 0)
    BEGIN
        SET @j = @j+5
    END
END
SET NOCOUNT OFF

CREATE NONCLUSTERED INDEX [IX_2_4_3] ON [#Example]
(
    [GroupID] ASC,
    [UniqueDate] ASC,
    [GroupDate] ASC
)
INCLUDE ([UniqueID])

-- Identify any UniqueDates that are greater than the GroupDate within their GroupID
SELECT UniqueID
     , GroupID
     , GroupDate
     , UniqueDate
FROM (
    SELECT UniqueID
         , GroupID
         , GroupDate
         , UniqueDate
         , MAX(UniqueDate) OVER (PARTITION BY GroupID) AS maxUniqueDate
    FROM #Example
    ) calc_maxUD
WHERE maxUniqueDate > GroupDate
    AND maxUniqueDate = UniqueDate

DROP TABLE #Example

dbfiddle qui


2
Se si desidera ottimizzare le prestazioni di una query, gli indici sulla tabella rappresentano una parte importante della domanda.
Daniel Hutmacher,

@DanielHutmacher Sono completamente d'accordo, anche se non ho intenzione di scaricare uno schema per il mio DWH e l'area di gestione temporanea, quindi questo è il meglio che posso fare entro limiti ragionevoli.
John Eisbrener,

Risposte:


9

Suppongo che non ci sia indice, dato che non ne hai fornito nessuno.

Immediatamente, il seguente indice eliminerà un operatore di ordinamento nel tuo piano, che altrimenti consumerebbe potenzialmente molta memoria:

CREATE INDEX IX ON #Example (GroupID, UniqueDate) INCLUDE (UniqueID, GroupDate);

La subquery non è un problema di prestazioni in questo caso. Semmai, vorrei cercare i modi per eliminare la funzione finestra (MAX ... OVER) per evitare il costrutto Nested Loop e Table Spool.

Con lo stesso indice, la seguente query può sembrare a prima vista meno efficiente e passa da due a tre scansioni nella tabella di base, ma elimina un numero enorme di letture internamente perché manca di operatori di spool. Immagino che continuerà a funzionare meglio, soprattutto se hai abbastanza core CPU e prestazioni IO sul tuo server:

SELECT e.UniqueID
     , e.GroupID
     , e.GroupDate
     , e.UniqueDate
FROM (
    SELECT GroupID, MAX(UniqueDate) AS maxUniqueDate
    FROM #Example
    GROUP BY GroupID) AS agg
INNER JOIN #Example AS e ON agg.GroupID=e.GroupID
WHERE agg.maxUniqueDate > e.GroupDate
    AND agg.maxUniqueDate = e.UniqueDate
OPTION (MERGE JOIN);

(Nota: ho aggiunto un MERGE JOINsuggerimento per le query, ma questo dovrebbe probabilmente accadere automaticamente se le tue statistiche sono in ordine. La migliore pratica è lasciare dei suggerimenti come questi se puoi.)


6
Si è brutto, ma il piano di esecuzione è più bella. Questa è la magia di linguaggi dichiarativi come T-SQL.
Daniel Hutmacher,

11

Quando e se si è in grado di eseguire l'aggiornamento da SQL Server 2012 a SQL Server 2016, è possibile sfruttare le prestazioni notevolmente migliorate (in particolare per gli aggregati di finestre senza cornice) fornite dal nuovo operatore di aggregazione della modalità batch.

Quasi tutti gli scenari di elaborazione dei dati di grandi dimensioni funzionano meglio con l'archiviazione columnstore rispetto al rowstore. Anche senza passare al columnstore per le tabelle di base, puoi comunque ottenere i vantaggi del nuovo operatore 2016 e dell'esecuzione in modalità batch creando un indice filtrato columnstore vuoto non cluster su una delle tabelle di base o unendo ridondantemente esterno a un columnstore organizzato tavolo.

Utilizzando la seconda opzione, la query diventa:

-- Just to get batch mode processing and the window aggregate operator
CREATE TABLE #Dummy (a integer NOT NULL, INDEX DummyCC CLUSTERED COLUMNSTORE);

-- Identify any UniqueDates that are greater than the GroupDate within their GroupID
SELECT
    calc_maxUD.UniqueID,
    calc_maxUD.GroupID,
    calc_maxUD.GroupDate,
    calc_maxUD.UniqueDate
FROM 
(
    SELECT
        E.UniqueID,
        E.GroupID,
        E.GroupDate,
        E.UniqueDate,
        maxUniqueDate = MAX(UniqueDate) OVER (
            PARTITION BY GroupID)
    FROM #Example AS E
    LEFT JOIN #Dummy AS D -- The only change to the original query
        ON 1 = 0
) AS calc_maxUD
WHERE 
    calc_maxUD.maxUniqueDate > calc_maxUD.GroupDate
    AND calc_maxUD.maxUniqueDate = calc_maxUD.UniqueDate;

db <> violino

Nota l'unica modifica alla query originale è la creazione di una tabella temporanea vuota e l'aggiunta del join sinistro. Il piano di esecuzione è:

piano aggregato della finestra in modalità batch

(58 row(s) affected)
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0
Table '#Example'. Scan count 1, logical reads 40, physical reads 0, read-ahead reads 0

Per ulteriori informazioni e opzioni, vedere l'eccellente serie di Itzik Ben-Gan, cosa è necessario sapere sull'operatore aggregato della finestra in modalità batch in SQL Server 2016 (in tre parti).


7

Sto solo per lanciare il vecchio Cross Apply là fuori:

SELECT e.*
    FROM #Example AS e
    CROSS APPLY ( SELECT TOP 1 e2.UniqueDate AS maxUniqueDate
                    FROM #Example AS e2
                    WHERE e2.GroupID = e.GroupID 
                    ORDER BY e2.UniqueDate DESC
                    ) AS ca
    WHERE ca.maxUniqueDate > e.GroupDate
        AND ca.maxUniqueDate = e.UniqueDate;

Con qualche tipo di indici, fa abbastanza bene.

CREATE CLUSTERED INDEX cx_whatever ON #Example (GroupID)

CREATE UNIQUE NONCLUSTERED INDEX ix_whatever ON #Example (GroupID, UniqueDate DESC, GroupDate)

Il tempo delle statistiche e io appaiono così (la tua query è il primo risultato)

Table 'Worktable'. Scan count 3, logical reads 28004, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table '#Example'. Scan count 1, logical reads 51, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 15 ms,  elapsed time = 20 ms.

Table '#Example'. Scan count 10001, logical reads 21336, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 16 ms,  elapsed time = 11 ms.

I piani di query sono qui (di nuovo, il tuo è il primo):

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

Perché preferisco questa versione? Evito le bobine. Se quelli iniziano a rovesciarsi su disco, diventerà brutto.

Ma potresti voler provare anche questo.

SELECT e.*
    FROM #Example AS e
    CROSS APPLY ( SELECT e2.UniqueDate AS maxUniqueDate
                    FROM #Example AS e2
                    WHERE e2.GroupID = e.GroupID 
                    ) AS ca
    WHERE ca.maxUniqueDate > e.GroupDate
        AND ca.maxUniqueDate = e.UniqueDate;

Se si tratta di un DW di grandi dimensioni, potresti preferire l'Hash Join e il filtro di riga nel join, anziché alla fine nella TOP 1query come operatore Filter.

Il piano è qui: https://www.brentozar.com/pastetheplan/?id=BkUF55ATx

Statistiche tempo e io qui:

Table 'Workfile'. 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 '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 '#Example'. Scan count 2, logical reads 84, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 16 ms,  elapsed time = 5 ms.

Spero che sia di aiuto!

Una modifica, basata sull'idea di @ ypercube, e un nuovo indice.

CREATE NONCLUSTERED INDEX ix_meh ON #Example (UniqueDate,GroupDate) INCLUDE (UniqueID,GroupID);

WITH t1 AS 
(
    SELECT DISTINCT
    e.GroupID ,
    MAX(UniqueDate) AS MaxUniqueDate
    FROM #Example AS e
    GROUP BY e.GroupID
)
SELECT *
FROM #Example AS e
CROSS APPLY (
SELECT *
FROM t1
    WHERE t1.MaxUniqueDate > e.GroupDate
        AND t1.MaxUniqueDate = e.UniqueDate
        AND t1.GroupID = e.GroupID
) ca

Ecco il tempo delle statistiche e io:

Table 'Workfile'. 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 '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 '#Example'. Scan count 2, logical reads 91, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 4 ms.

Ecco il piano:

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


Sembra che il mio esempio sia stato un po 'troppo pulito, in quanto vi sono scenari in cui posso avere più date uniche maggiori della data del gruppo nel mio ambiente reale. Questa condizione invalida la seconda query Cross Apply, ma gli altri approcci funzionano entrambi senza problemi. Grazie per alcune altre opzioni!
John Eisbrener,

4

Vorrei dare un'occhiata top with ties

Se GroupDateè lo stesso per GroupIdallora:

select top 1 with ties 
   UniqueID
 , GroupID
 , GroupDate
 , UniqueDate
from #Example
where UniqueDate > GroupDate
order by row_number() over (partition by GroupId order by UniqueDate desc)

Altro: utilizzo top with tiesin un'espressione di tabella comune

with cte as (
  select top 1 with ties 
      UniqueID
    , GroupID
    , GroupDate
    , UniqueDate
  from #Example
  order by row_number() over (partition by GroupId order by UniqueDate desc)
)
select *
from cte
where UniqueDate > GroupDate

dbfiddle: http://dbfiddle.uk/?rdbms=sqlserver_2016&fiddle=c058994c2f5f3d99b212f06e1dae9fd3

Query originale

Table 'Worktable'. Scan count 3, logical reads 28001, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table '#Example____________________________________________________________________________________________________________0000000000CB'. Scan count 1, logical reads 43, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

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

vs top with tiesin un'espressione di tabella comune

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 '#Example____________________________________________________________________________________________________________0000000000CB'. Scan count 1, logical reads 43, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 16 ms,  elapsed time = 15 ms.

4

Quindi ho fatto alcune analisi sui vari approcci pubblicati finora e, nel mio ambiente, sembra che l'approccio di Daniel vince costantemente sui tempi di esecuzione. Sorprendentemente (per me) il terzo approccio CROSS APPLY di sp_BlitzErik non era poi così indietro. Ecco gli output se qualcuno è interessato, ma grazie a TON per tutti gli approcci alternativi. Ho imparato di più scavando nelle risposte a questa domanda di quante ne abbia da un bel po '!

Windowed Function - baseline metric

(10406 row(s) affected)
Table 'DateDim'. Scan count 9, logical reads 791, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TableFact'. Scan count 9, logical reads 140181, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. 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 'Worktable'. Scan count 89815, logical reads 42553550, physical reads 0, read-ahead reads 84586, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table01Dim'. Scan count 9, logical reads 7688, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table02Dim'. Scan count 9, logical reads 7819, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 87753 ms,  elapsed time = 13031 ms.
Warning: Null value is eliminated by an aggregate or other SET operation.


Basic Aggregated Subquery - Daniel Hutmacher

(10406 row(s) affected)
Table 'DateDim'. Scan count 18, logical reads 1194, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TableFact'. Scan count 18, logical reads 280362, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 48, logical reads 82408, physical reads 9629, read-ahead reads 72779, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 89791, logical reads 6861425, physical reads 0, read-ahead reads 14565, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table01Dim'. Scan count 9, logical reads 7688, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table02Dim'. Scan count 18, logical reads 15726, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 40527 ms,  elapsed time = 6182 ms.
Warning: Null value is eliminated by an aggregate or other SET operation.


CROSS APPLY Operation A - sp_BlitzErik

(10406 row(s) affected)
Table 'DateDim'. Scan count 9, logical reads 6199331, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TableFact'. Scan count 3099273, logical reads 12844012, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table01Dim'. Scan count 3109676, logical reads 9350502, physical reads 0, read-ahead reads 0, 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 'Table02Dim'. Scan count 3109676, logical reads 9482456, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. 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 '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 '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.

 SQL Server Execution Times:
   CPU time = 132632 ms,  elapsed time = 20955 ms.


CROSS APPLY Operation C - sp_BlitzErik

(10406 row(s) affected)
Table 'DateDim'. Scan count 18, logical reads 1194, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TableFact'. Scan count 18, logical reads 280362, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 56, logical reads 92800, physical reads 10872, read-ahead reads 81928, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 89791, logical reads 6861425, physical reads 0, read-ahead reads 14563, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table01Dim'. Scan count 18, logical reads 15376, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table02Dim'. Scan count 18, logical reads 15726, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 46082 ms,  elapsed time = 6804 ms.
Warning: Null value is eliminated by an aggregate or other SET operation.


TOP 1 WITH TIES - B - SqlZim

(10406 row(s) affected)
Table 'DateDim'. Scan count 9, logical reads 791, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TableFact'. Scan count 9, logical reads 140181, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. 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 'Worktable'. Scan count 89791, logical reads 6866304, physical reads 0, read-ahead reads 93468, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table01Dim'. Scan count 9, logical reads 7688, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table02Dim'. Scan count 9, logical reads 7835, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 79406 ms,  elapsed time = 15852 ms.

Stavo solo osservando come si sarebbero accumulate le opzioni pubblicate se avessi portato il tuo esempio a 100.000 righe e aggiunto i suggerimenti per l'indice di tutti. Sembra piuttosto rappresentativo dei tuoi risultati reali. Sembra la mia versione di top with tiesfibbie con così tante file. dbfiddle.uk/…
SqlZim
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.