Sqlserver
 sql >> Database >  >> RDS >> Sqlserver

subquery o leftjoin con il gruppo di cui uno è più veloce?

Un'ottima risorsa per il calcolo dei totali parziali in SQL Server è questo documento da Itzik Ben Gan che è stato inviato al team di SQL Server come parte della sua campagna per avere il OVER clausola estesa ulteriormente dalla sua implementazione iniziale di SQL Server 2005. In esso mostra come una volta entrati in decine di migliaia di righe, i cursori eseguono soluzioni basate su set. SQL Server 2012 ha effettivamente esteso OVER clausola che semplifica notevolmente questo tipo di query.

SELECT col1,
       SUM(col1) OVER (ORDER BY ind ROWS UNBOUNDED PRECEDING)
FROM   @tmp 

Poiché sei su SQL Server 2005, tuttavia, questo non è disponibile per te.

Adam Machanic mostra qui come utilizzare il CLR per migliorare le prestazioni dei cursori TSQL standard.

Per questa definizione di tabella

CREATE TABLE RunningTotals
(
ind int identity(1,1) primary key,
col1 int
)

Creo tabelle con 2.000 e 10.000 righe in un database con ALLOW_SNAPSHOT_ISOLATION ON e uno con questa impostazione (il motivo è perché i miei risultati iniziali erano in un DB con l'impostazione attiva che ha portato a un aspetto sconcertante dei risultati).

Gli indici cluster per tutte le tabelle avevano solo 1 pagina radice. Il numero di pagine foglia per ciascuna è mostrato di seguito.

+-------------------------------+-----------+------------+
|                               | 2,000 row | 10,000 row |
+-------------------------------+-----------+------------+
| ALLOW_SNAPSHOT_ISOLATION OFF  |         5 |         22 |
| ALLOW_SNAPSHOT_ISOLATION ON   |         8 |         39 |
+-------------------------------+-----------+------------+

Ho testato i seguenti casi (i link mostrano i piani di esecuzione)

  1. Partecipa a sinistra e raggruppa per
  2. Subquery correlata Piano di 2000 righe ,Piano da 10000 righe
  3. CTE dalla risposta (aggiornata) di Mikael
  4. CTE di seguito

Il motivo per l'inclusione dell'opzione CTE aggiuntiva era al fine di fornire una soluzione CTE che avrebbe comunque funzionato se il ind la colonna non era sequenziale garantita.

SET STATISTICS IO ON;
SET STATISTICS TIME ON;
DECLARE @col1 int, @sumcol1 bigint;

WITH    RecursiveCTE
AS      (
        SELECT TOP 1 ind, col1, CAST(col1 AS BIGINT) AS Total
        FROM RunningTotals
        ORDER BY ind
        UNION   ALL
        SELECT  R.ind, R.col1, R.Total
        FROM    (
                SELECT  T.*,
                        T.col1 + Total AS Total,
                        rn = ROW_NUMBER() OVER (ORDER BY T.ind)
                FROM    RunningTotals T
                JOIN    RecursiveCTE R
                        ON  R.ind < T.ind
                ) R
        WHERE   R.rn = 1
        )
SELECT  @col1 =col1, @sumcol1=Total
FROM    RecursiveCTE
OPTION  (MAXRECURSION 0);

Tutte le query avevano un CAST(col1 AS BIGINT) aggiunto per evitare errori di overflow in fase di esecuzione. Inoltre per tutti ho assegnato i risultati alle variabili come sopra per eliminare dalla considerazione il tempo speso per inviare i risultati.

Risultati

+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
|                  |          |        |          Base Table        |         Work Table         |     Time        |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
|                  | Snapshot | Rows   | Scan count | logical reads | Scan count | logical reads | cpu   | elapsed |
| Group By         | On       | 2,000  | 2001       | 12709         |            |               | 1469  | 1250    |
|                  | On       | 10,000 | 10001      | 216678        |            |               | 30906 | 30963   |
|                  | Off      | 2,000  | 2001       | 9251          |            |               | 1140  | 1160    |
|                  | Off      | 10,000 | 10001      | 130089        |            |               | 29906 | 28306   |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| Sub Query        | On       | 2,000  | 2001       | 12709         |            |               | 844   | 823     |
|                  | On       | 10,000 | 2          | 82            | 10000      | 165025        | 24672 | 24535   |
|                  | Off      | 2,000  | 2001       | 9251          |            |               | 766   | 999     |
|                  | Off      | 10,000 | 2          | 48            | 10000      | 165025        | 25188 | 23880   |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| CTE No Gaps      | On       | 2,000  | 0          | 4002          | 2          | 12001         | 78    | 101     |
|                  | On       | 10,000 | 0          | 20002         | 2          | 60001         | 344   | 342     |
|                  | Off      | 2,000  | 0          | 4002          | 2          | 12001         | 62    | 253     |
|                  | Off      | 10,000 | 0          | 20002         | 2          | 60001         | 281   | 326     |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| CTE Alllows Gaps | On       | 2,000  | 2001       | 4009          | 2          | 12001         | 47    | 75      |
|                  | On       | 10,000 | 10001      | 20040         | 2          | 60001         | 312   | 413     |
|                  | Off      | 2,000  | 2001       | 4006          | 2          | 12001         | 94    | 90      |
|                  | Off      | 10,000 | 10001      | 20023         | 2          | 60001         | 313   | 349     |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+

Sia la sottoquery correlata che il GROUP BY la versione utilizza join di loop nidificati "triangolari" guidati da una scansione dell'indice cluster su RunningTotals tabella (T1 ) e, per ogni riga restituita da quella scansione, ricerca nella tabella (T2 ) adesione automatica su T2.ind<=T1.ind .

Ciò significa che le stesse righe vengono elaborate ripetutamente. Quando il T1.ind=1000 riga viene elaborata il self join recupera e somma tutte le righe con un ind <= 1000 , quindi per la riga successiva dove T1.ind=1001 le stesse 1000 righe vengono recuperate di nuovo e sommato con una riga aggiuntiva e così via.

Il numero totale di tali operazioni per una tabella di 2.000 righe è 2.001.000, per 10.000 righe 50.005.000 o più in generale (n² + n) / 2 che chiaramente cresce in modo esponenziale.

Nel caso di 2.000 righe la differenza principale tra il GROUP BY e le versioni delle sottoquery sono che la prima ha lo stream aggregato dopo il join e quindi ha tre colonne che vi alimentano (T1.ind , T2.col1 , T2.col1 ) e un GROUP BY proprietà di T1.ind mentre quest'ultimo viene calcolato come un aggregato scalare, con l'aggregato del flusso prima del join, ha solo T2.col1 alimentandolo e non ha GROUP BY proprietà impostata a tutti. Questa disposizione più semplice può essere considerata come un vantaggio misurabile in termini di tempo CPU ridotto.

Per il caso di 10.000 righe è presente un'ulteriore differenza nel piano delle sottoquery. Aggiunge uno spool desideroso che copia tutti i ind,cast(col1 as bigint) valori in tempdb . Nel caso in cui l'isolamento dello snapshot sia su questo risulta più compatto rispetto alla struttura dell'indice cluster e l'effetto netto è quello di ridurre il numero di letture di circa il 25% (poiché la tabella di base conserva molto spazio vuoto per le informazioni sul controllo delle versioni), quando questa opzione è disattivata, risulta meno compatta (presumibilmente a causa di bigint rispetto a int differenza) e più letture risultano. Ciò riduce il divario tra la query secondaria e il gruppo per versioni, ma la query secondaria continua a vincere.

Il chiaro vincitore, tuttavia, è stato il CTE ricorsivo. Per la versione "no gap" le letture logiche dalla tabella di base sono ora 2 x (n + 1) che riflette il n index cerca nell'indice di livello 2 per recuperare tutte le righe più quella aggiuntiva alla fine che non restituisce nulla e termina la ricorsione. Ciò significava comunque 20.002 letture per elaborare una tabella di 22 pagine!

Le letture della tabella di lavoro logica per la versione CTE ricorsiva sono molto elevate. Sembra funzionare con 6 letture della tabella di lavoro per riga di origine. Questi provengono dallo spool dell'indice che memorizza l'output della riga precedente, quindi viene letto di nuovo nell'iterazione successiva (buona spiegazione di Umachandar Jayachandran qui ). Nonostante il numero elevato, questo è ancora il miglior rendimento.