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

Calcola il totale corrente / il saldo corrente

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:

  1. SUM() OVER() ... ROWS , se del 2012 o superiore
  2. Metodo CLR, se possibile
  3. Primo metodo CTE ricorsivo, se possibile
  4. Cursore
  5. Gli altri metodi CTE ricorsivi
  6. Aggiornamento eccentrico
  7. 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