Per coloro che non utilizzano SQL Server 2012 o versioni successive, un cursore è probabilmente il supportato più efficiente e garantito metodo al di fuori di CLR. Esistono altri approcci come l '"aggiornamento eccentrico" che può essere leggermente più veloce ma non garantito per funzionare in futuro, e ovviamente approcci basati su set con profili di prestazioni iperbolici man mano che la tabella diventa più grande e metodi CTE ricorsivi che spesso richiedono #tempdb I/O o provocano fuoriuscite che producono più o meno lo stesso impatto.
INNER JOIN - non farlo:
L'approccio lento e basato sugli insiemi ha la forma:
SELECT t1.TID, t1.amt, RunningTotal = SUM(t2.amt)
FROM dbo.Transactions AS t1
INNER JOIN dbo.Transactions AS t2
ON t1.TID >= t2.TID
GROUP BY t1.TID, t1.amt
ORDER BY t1.TID;
Il motivo per cui questo è lento? Man mano che la tabella diventa più grande, ogni riga incrementale richiede la lettura di n-1 righe nella tabella. Questo è esponenziale e legato a errori, timeout o semplicemente utenti arrabbiati.
Subquery correlata:non eseguire nemmeno questa operazione:
Il modulo di sottoquery è altrettanto doloroso per ragioni altrettanto dolorose.
SELECT TID, amt, RunningTotal = amt + COALESCE(
(
SELECT SUM(amt)
FROM dbo.Transactions AS i
WHERE i.TID < o.TID), 0
)
FROM dbo.Transactions AS o
ORDER BY TID;
Aggiornamento stravagante:fallo a tuo rischio:
Il metodo "eccentrico aggiornamento" è più efficiente di quello sopra, ma il comportamento non è documentato, non ci sono garanzie sull'ordine e il comportamento potrebbe funzionare oggi ma potrebbe interrompersi in futuro. Includo questo perché è un metodo popolare ed è efficiente, ma ciò non significa che lo approvi. Il motivo principale per cui ho persino risposto a questa domanda invece di chiuderla come duplicato è perché l'altra domanda ha un aggiornamento bizzarro come risposta accettata.
DECLARE @t TABLE
(
TID INT PRIMARY KEY,
amt INT,
RunningTotal INT
);
DECLARE @RunningTotal INT = 0;
INSERT @t(TID, amt, RunningTotal)
SELECT TID, amt, RunningTotal = 0
FROM dbo.Transactions
ORDER BY TID;
UPDATE @t
SET @RunningTotal = RunningTotal = @RunningTotal + amt
FROM @t;
SELECT TID, amt, RunningTotal
FROM @t
ORDER BY TID;
CTE ricorsivi
Questo primo si basa su TID per essere contiguo, senza lacune:
;WITH x AS
(
SELECT TID, amt, RunningTotal = amt
FROM dbo.Transactions
WHERE TID = 1
UNION ALL
SELECT y.TID, y.amt, x.RunningTotal + y.amt
FROM x
INNER JOIN dbo.Transactions AS y
ON y.TID = x.TID + 1
)
SELECT TID, amt, RunningTotal
FROM x
ORDER BY TID
OPTION (MAXRECURSION 10000);
Se non puoi fare affidamento su questo, puoi usare questa variazione, che crea semplicemente una sequenza contigua usando ROW_NUMBER()
:
;WITH y AS
(
SELECT TID, amt, rn = ROW_NUMBER() OVER (ORDER BY TID)
FROM dbo.Transactions
), x AS
(
SELECT TID, rn, amt, rt = amt
FROM y
WHERE rn = 1
UNION ALL
SELECT y.TID, y.rn, y.amt, x.rt + y.amt
FROM x INNER JOIN y
ON y.rn = x.rn + 1
)
SELECT TID, amt, RunningTotal = rt
FROM x
ORDER BY x.rn
OPTION (MAXRECURSION 10000);
A seconda della dimensione dei dati (ad es. colonne di cui non siamo a conoscenza), potresti ottenere prestazioni complessive migliori riempiendo prima le colonne pertinenti solo in una tabella #temp ed elaborandole in base a quella anziché alla tabella di base:
CREATE TABLE #x
(
rn INT PRIMARY KEY,
TID INT,
amt INT
);
INSERT INTO #x (rn, TID, amt)
SELECT ROW_NUMBER() OVER (ORDER BY TID),
TID, amt
FROM dbo.Transactions;
;WITH x AS
(
SELECT TID, rn, amt, rt = amt
FROM #x
WHERE rn = 1
UNION ALL
SELECT y.TID, y.rn, y.amt, x.rt + y.amt
FROM x INNER JOIN #x AS y
ON y.rn = x.rn + 1
)
SELECT TID, amt, RunningTotal = rt
FROM x
ORDER BY TID
OPTION (MAXRECURSION 10000);
DROP TABLE #x;
Solo il primo metodo CTE fornirà prestazioni che rivaleggiano con l'eccentrico aggiornamento, ma fa una grande ipotesi sulla natura dei dati (nessuna lacuna). Gli altri due metodi ricadranno e in questi casi puoi anche utilizzare un cursore (se non puoi utilizzare CLR e non sei ancora su SQL Server 2012 o versioni successive).
Cursore
A tutti viene detto che i cursori sono malvagi e che dovrebbero essere evitati a tutti i costi, ma questo in realtà batte le prestazioni della maggior parte degli altri metodi supportati ed è più sicuro dell'eccentrico aggiornamento. Gli unici che preferisco alla soluzione del cursore sono i metodi 2012 e CLR (sotto):
CREATE TABLE #x
(
TID INT PRIMARY KEY,
amt INT,
rt INT
);
INSERT #x(TID, amt)
SELECT TID, amt
FROM dbo.Transactions
ORDER BY TID;
DECLARE @rt INT, @tid INT, @amt INT;
SET @rt = 0;
DECLARE c CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR SELECT TID, amt FROM #x ORDER BY TID;
OPEN c;
FETCH c INTO @tid, @amt;
WHILE @@FETCH_STATUS = 0
BEGIN
SET @rt = @rt + @amt;
UPDATE #x SET rt = @rt WHERE TID = @tid;
FETCH c INTO @tid, @amt;
END
CLOSE c; DEALLOCATE c;
SELECT TID, amt, RunningTotal = rt
FROM #x
ORDER BY TID;
DROP TABLE #x;
SQL Server 2012 o versioni successive
Le nuove funzioni della finestra introdotte in SQL Server 2012 rendono questa attività molto più semplice (e funziona anche meglio di tutti i metodi precedenti):
SELECT TID, amt,
RunningTotal = SUM(amt) OVER (ORDER BY TID ROWS UNBOUNDED PRECEDING)
FROM dbo.Transactions
ORDER BY TID;
Nota che su set di dati più grandi, scoprirai che quanto sopra funziona molto meglio di una delle due seguenti opzioni, poiché RANGE utilizza uno spool su disco (e l'impostazione predefinita utilizza RANGE). Tuttavia è anche importante notare che il comportamento e i risultati possono differire, quindi assicurati che entrambi restituiscano risultati corretti prima di decidere tra di loro in base a questa differenza.
SELECT TID, amt,
RunningTotal = SUM(amt) OVER (ORDER BY TID)
FROM dbo.Transactions
ORDER BY TID;
SELECT TID, amt,
RunningTotal = SUM(amt) OVER (ORDER BY TID RANGE UNBOUNDED PRECEDING)
FROM dbo.Transactions
ORDER BY TID;
CLR
Per completezza, offro un collegamento al metodo CLR di Pavel Pawlowski, che è di gran lunga il metodo preferibile nelle versioni precedenti a SQL Server 2012 (ma ovviamente non 2000).
http://www.pawlowski.cz/2010/09/sql-server-and-fastest-running-totals-using-clr/
Conclusione
Se utilizzi SQL Server 2012 o versioni successive, la scelta è ovvia:usa il nuovo SUM() OVER()
costruire (con ROWS
rispetto a RANGE
). Per le versioni precedenti, ti consigliamo di confrontare le prestazioni degli approcci alternativi sul tuo schema, dati e, tenendo conto dei fattori non correlati alle prestazioni, determinare quale approccio è giusto per te. Potrebbe benissimo essere l'approccio CLR. Ecco i miei consigli, in ordine di preferenza:
SUM() OVER() ... ROWS
, se del 2012 o superiore- Metodo CLR, se possibile
- Primo metodo CTE ricorsivo, se possibile
- Cursore
- Gli altri metodi CTE ricorsivi
- Aggiornamento eccentrico
- Partecipa e/o sottoquery correlata
Per ulteriori informazioni sui confronti delle prestazioni di questi metodi, vedere questa domanda su http://dba.stackexchange.com:
https://dba.stackexchange.com/questions/19507/running-total-with-count
Ho anche pubblicato sul blog maggiori dettagli su questi confronti qui:
http://www.sqlperformance.com/2012/07/t-sql-queries/running-totals
Anche per i totali parziali raggruppati/partizionati, vedere i seguenti post:
http://sqlperformance.com/2014/01/t-sql-queries/grouped-running-totals
Il partizionamento genera una query sui totali parziali
Totali parziali multipli con raggruppamento per