Sqlserver
 sql >> Datenbank >  >> RDS >> Sqlserver

Berechnen Sie die laufende Summe / den laufenden Saldo

Für diejenigen, die SQL Server 2012 oder höher nicht verwenden, ist ein Cursor wahrscheinlich am effizientesten unterstützt und garantiert Methode außerhalb von CLR. Es gibt andere Ansätze wie das "skurrile Update", das geringfügig schneller sein kann, aber nicht garantiert in Zukunft funktioniert, und natürlich satzbasierte Ansätze mit hyperbolischen Leistungsprofilen, wenn die Tabelle größer wird, und rekursive CTE-Methoden, die häufig direkt erfordern #tempdb E/A oder zu Verschüttungen führen, die ungefähr die gleiche Auswirkung haben.

INNER JOIN - tun Sie dies nicht:

Der langsame, mengenbasierte Ansatz hat die Form:

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;

Der Grund dafür ist langsam? Wenn die Tabelle größer wird, erfordert jede inkrementelle Zeile das Lesen von n-1 Zeilen in der Tabelle. Dies ist exponentiell und für Ausfälle, Zeitüberschreitungen oder einfach nur verärgerte Benutzer bestimmt.

Korrelierte Unterabfrage - tun Sie dies auch nicht:

Das Unterabfrageformular ist aus ähnlich schmerzhaften Gründen ähnlich schmerzhaft.

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;

Skurriles Update - tun Sie dies auf eigene Gefahr:

Die „skurrile Update“-Methode ist effizienter als die obige, aber das Verhalten ist nicht dokumentiert, es gibt keine Garantien für die Reihenfolge, und das Verhalten könnte heute funktionieren, könnte aber in Zukunft brechen. Ich füge dies hinzu, weil es eine beliebte und effiziente Methode ist, aber das bedeutet nicht, dass ich es befürworte. Der Hauptgrund, warum ich diese Frage überhaupt beantwortet habe, anstatt sie als Duplikat zu schließen, ist, dass die andere Frage ein skurriles Update als akzeptierte Antwort hat.

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;

Rekursive CTEs

Dieser erste verlässt sich darauf, dass TID zusammenhängend und ohne Lücken ist:

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

Wenn Sie sich darauf nicht verlassen können, können Sie diese Variante verwenden, die einfach eine zusammenhängende Sequenz mit ROW_NUMBER() aufbaut :

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

Abhängig von der Größe der Daten (z. B. Spalten, von denen wir nichts wissen), finden Sie möglicherweise eine bessere Gesamtleistung, indem Sie die relevanten Spalten zuerst nur in eine #temp-Tabelle füllen und diese anstelle der Basistabelle verarbeiten:

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;

Nur die erste CTE-Methode bietet eine Leistung, die mit dem skurrilen Update konkurriert, aber sie macht eine große Annahme über die Art der Daten (keine Lücken). Die anderen beiden Methoden greifen zurück und in diesen Fällen können Sie auch einen Cursor verwenden (wenn Sie CLR nicht verwenden können und Sie noch nicht auf SQL Server 2012 oder höher sind).

Cursor

Allen wird gesagt, dass Cursor böse sind und um jeden Preis vermieden werden sollten, aber dies übertrifft tatsächlich die Leistung der meisten anderen unterstützten Methoden und ist sicherer als das schrullige Update. Die einzigen, die ich gegenüber der Cursor-Lösung bevorzuge, sind die 2012- und CLR-Methoden (unten):

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 oder höher

Neue Fensterfunktionen, die in SQL Server 2012 eingeführt wurden, machen diese Aufgabe viel einfacher (und sie ist auch leistungsfähiger als alle oben genannten Methoden):

SELECT TID, amt, 
  RunningTotal = SUM(amt) OVER (ORDER BY TID ROWS UNBOUNDED PRECEDING)
FROM dbo.Transactions
ORDER BY TID;

Beachten Sie, dass Sie bei größeren Datensätzen feststellen werden, dass die obige Option viel besser funktioniert als die beiden folgenden Optionen, da RANGE einen Spool auf der Festplatte verwendet (und die Standardeinstellung RANGE verwendet). Es ist jedoch auch wichtig zu beachten, dass das Verhalten und die Ergebnisse unterschiedlich sein können, stellen Sie also sicher, dass beide korrekte Ergebnisse zurückgeben, bevor Sie sich aufgrund dieses Unterschieds zwischen ihnen entscheiden.

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

Der Vollständigkeit halber biete ich einen Link zur CLR-Methode von Pavel Pawlowski an, die bei Versionen vor SQL Server 2012 (aber offensichtlich nicht 2000) bei weitem die bevorzugte Methode ist.

http://www.pawlowski.cz/2010/09/sql-server-and-fastest-running-totals-using-clr/

Schlussfolgerung

Wenn Sie SQL Server 2012 oder höher verwenden, ist die Wahl offensichtlich - verwenden Sie das neue SUM() OVER() konstruieren (mit ROWS vs. RANGE ). Bei früheren Versionen sollten Sie die Leistung der alternativen Ansätze für Ihr Schema und Ihre Daten vergleichen und – unter Berücksichtigung nicht leistungsbezogener Faktoren – bestimmen, welcher Ansatz der richtige für Sie ist. Es kann sehr gut der CLR-Ansatz sein. Hier sind meine Empfehlungen in der Reihenfolge ihrer Präferenz:

  1. SUM() OVER() ... ROWS , ab 2012
  2. CLR-Methode, wenn möglich
  3. Erste rekursive CTE-Methode, wenn möglich
  4. Cursor
  5. Die anderen rekursiven CTE-Methoden
  6. Skurriles Update
  7. Join und/oder korrelierte Unterabfrage

Weitere Informationen mit Leistungsvergleichen dieser Methoden finden Sie in dieser Frage auf http://dba.stackexchange.com:

https://dba.stackexchange.com/questions/19507/running-total-with-count

Ich habe auch mehr Details über diese Vergleiche hier gebloggt:

http://www.sqlperformance.com/2012/07/t-sql-queries/running-totals

Siehe auch für gruppierte/partitionierte laufende Summen die folgenden Posts:

http://sqlperformance.com/2014/01/t-sql-queries/grouped-running-totals

Die Partitionierung führt zu einer laufenden Summenabfrage

Mehrere laufende Summen mit Gruppieren nach