Berechnen Sie eine laufende Summe in SQL Server


169

Stellen Sie sich die folgende Tabelle vor (aufgerufen TestTable):

id     somedate    somevalue
--     --------    ---------
45     01/Jan/09   3
23     08/Jan/09   5
12     02/Feb/09   0
77     14/Feb/09   7
39     20/Feb/09   34
33     02/Mar/09   6

Ich möchte eine Abfrage, die eine laufende Summe in Datumsreihenfolge zurückgibt, wie:

id     somedate    somevalue  runningtotal
--     --------    ---------  ------------
45     01/Jan/09   3          3
23     08/Jan/09   5          8
12     02/Feb/09   0          8
77     14/Feb/09   7          15  
39     20/Feb/09   34         49
33     02/Mar/09   6          55

Ich weiß, dass es in SQL Server 2000/2005/2008 verschiedene Möglichkeiten gibt, dies zu tun .

Ich interessiere mich besonders für diese Art von Methode, die den Trick Aggregating-Set-Statement verwendet:

INSERT INTO @AnotherTbl(id, somedate, somevalue, runningtotal) 
   SELECT id, somedate, somevalue, null
   FROM TestTable
   ORDER BY somedate

DECLARE @RunningTotal int
SET @RunningTotal = 0

UPDATE @AnotherTbl
SET @RunningTotal = runningtotal = @RunningTotal + somevalue
FROM @AnotherTbl

... das ist sehr effizient, aber ich habe gehört, dass es Probleme gibt, weil Sie nicht unbedingt garantieren können, dass die UPDATEAnweisung die Zeilen in der richtigen Reihenfolge verarbeitet. Vielleicht können wir einige endgültige Antworten zu diesem Thema bekommen.

Aber vielleicht gibt es andere Möglichkeiten, die die Leute vorschlagen können?

edit: Jetzt mit einer SqlFiddle mit dem Setup und dem 'Update Trick' Beispiel oben


blogs.msdn.com/sqltips/archive/2005/07/20/441053.aspx Fügen Sie Ihrem Update eine Bestellung hinzu ... und Sie erhalten eine Garantie.
Simon D

Aber Order by kann nicht auf eine UPDATE-Anweisung angewendet werden ... oder?
Codeulike

Siehe auch sqlperformance.com/2012/07/t-sql-queries/running-totals, insbesondere wenn Sie SQL Server 2012 verwenden.
Aaron Bertrand

Antworten:


133

Aktualisieren Sie , wenn Sie SQL Server 2012 ausführen, folgende Informationen : https://stackoverflow.com/a/10309947

Das Problem ist, dass die SQL Server-Implementierung der Over-Klausel etwas eingeschränkt ist .

Mit Oracle (und ANSI-SQL) können Sie Folgendes tun:

 SELECT somedate, somevalue,
  SUM(somevalue) OVER(ORDER BY somedate 
     ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) 
          AS RunningTotal
  FROM Table

SQL Server bietet Ihnen keine saubere Lösung für dieses Problem. Mein Bauch sagt mir, dass dies einer der seltenen Fälle ist, in denen ein Cursor am schnellsten ist, obwohl ich ein Benchmarking für große Ergebnisse durchführen muss.

Der Update-Trick ist praktisch, aber ich finde ihn ziemlich zerbrechlich. Wenn Sie eine vollständige Tabelle aktualisieren, wird diese anscheinend in der Reihenfolge des Primärschlüssels ausgeführt. Wenn Sie also Ihr Datum als aufsteigenden Primärschlüssel festlegen, werden Sie dies tunprobably sicher. Sie verlassen sich jedoch auf ein undokumentiertes SQL Server-Implementierungsdetail (auch wenn die Abfrage von zwei Prozessen ausgeführt wird, frage ich mich, was passieren wird, siehe: MAXDOP):

Vollständiges Arbeitsprobe:

drop table #t 
create table #t ( ord int primary key, total int, running_total int)

insert #t(ord,total)  values (2,20)
-- notice the malicious re-ordering 
insert #t(ord,total) values (1,10)
insert #t(ord,total)  values (3,10)
insert #t(ord,total)  values (4,1)

declare @total int 
set @total = 0
update #t set running_total = @total, @total = @total + total 

select * from #t
order by ord 

ord         total       running_total
----------- ----------- -------------
1           10          10
2           20          30
3           10          40
4           1           41

Sie haben nach einem Benchmark gefragt, dies ist der Tiefpunkt.

Der schnellste sichere Weg, dies zu tun, wäre der Cursor. Er ist eine Größenordnung schneller als die korrelierte Unterabfrage von Cross-Join.

Der absolut schnellste Weg ist der UPDATE-Trick. Ich mache mir nur Sorgen, dass ich nicht sicher bin, ob das Update unter allen Umständen linear verläuft. Die Abfrage enthält nichts, was dies ausdrücklich sagt.

Unterm Strich würde ich für den Produktionscode mit dem Cursor gehen.

Testdaten:

create table #t ( ord int primary key, total int, running_total int)

set nocount on 
declare @i int
set @i = 0 
begin tran
while @i < 10000
begin
   insert #t (ord, total) values (@i,  rand() * 100) 
    set @i = @i +1
end
commit

Test 1:

SELECT ord,total, 
    (SELECT SUM(total) 
        FROM #t b 
        WHERE b.ord <= a.ord) AS b 
FROM #t a

-- CPU 11731, Reads 154934, Duration 11135 

Test 2:

SELECT a.ord, a.total, SUM(b.total) AS RunningTotal 
FROM #t a CROSS JOIN #t b 
WHERE (b.ord <= a.ord) 
GROUP BY a.ord,a.total 
ORDER BY a.ord

-- CPU 16053, Reads 154935, Duration 4647

Test 3:

DECLARE @TotalTable table(ord int primary key, total int, running_total int)

DECLARE forward_cursor CURSOR FAST_FORWARD 
FOR 
SELECT ord, total
FROM #t 
ORDER BY ord


OPEN forward_cursor 

DECLARE @running_total int, 
    @ord int, 
    @total int
SET @running_total = 0

FETCH NEXT FROM forward_cursor INTO @ord, @total 
WHILE (@@FETCH_STATUS = 0)
BEGIN
     SET @running_total = @running_total + @total
     INSERT @TotalTable VALUES(@ord, @total, @running_total)
     FETCH NEXT FROM forward_cursor INTO @ord, @total 
END

CLOSE forward_cursor
DEALLOCATE forward_cursor

SELECT * FROM @TotalTable

-- CPU 359, Reads 30392, Duration 496

Test 4:

declare @total int 
set @total = 0
update #t set running_total = @total, @total = @total + total 

select * from #t

-- CPU 0, Reads 58, Duration 139

1
Vielen Dank. Ihr Codebeispiel soll also zeigen, dass es sich in der Reihenfolge des Primärschlüssels summiert, nehme ich an. Es wäre interessant zu wissen, ob Cursor für größere Datenmengen immer noch effizienter sind als Joins.
Codeulike

1
Ich habe gerade den CTE @Martin getestet, nichts kommt dem Update-Trick nahe - der Cursor scheint beim Lesen niedriger zu sein. Hier ist ein Profiler Trace i.stack.imgur.com/BbZq3.png
Sam Saffron

3
@ Martin Denali wird eine ziemlich gute
Sam Saffron

1
+1 für all die Arbeit, die in diese Antwort gesteckt wurde - ich liebe die UPDATE-Option; Kann eine Partition in dieses UPDATE-Skript integriert werden? Wenn es beispielsweise ein zusätzliches Feld "Autofarbe" gäbe, könnte dieses Skript laufende Summen innerhalb jeder "Autofarbe" -Partition zurückgeben?
Whytheq

2
Die erste Antwort (Oracle (und ANSI-SQL)) funktioniert jetzt in SQL Server 2017. Vielen Dank, sehr elegant!
DaniDev

121

In SQL Server 2012 können Sie SUM () mit der OVER () -Klausel verwenden.

select id,
       somedate,
       somevalue,
       sum(somevalue) over(order by somedate rows unbounded preceding) as runningtotal
from TestTable

SQL Fiddle


40

Während Sam Saffron großartige Arbeit daran geleistet hat, hat er für dieses Problem immer noch keinen rekursiven allgemeinen Tabellenausdruckcode bereitgestellt. Und für uns, die mit SQL Server 2008 R2 und nicht mit Denali arbeiten, ist dies immer noch der schnellste Weg, um insgesamt ausgeführt zu werden. Es ist ungefähr zehnmal schneller als der Cursor auf meinem Arbeitscomputer für 100000 Zeilen und es ist auch eine Inline-Abfrage.
Also, hier ist es (ich nehme an, dass es eine ordSpalte in der Tabelle gibt und die fortlaufende Nummer ohne Lücken, für eine schnelle Verarbeitung sollte es auch eine eindeutige Einschränkung für diese Nummer geben):

;with 
CTE_RunningTotal
as
(
    select T.ord, T.total, T.total as running_total
    from #t as T
    where T.ord = 0
    union all
    select T.ord, T.total, T.total + C.running_total as running_total
    from CTE_RunningTotal as C
        inner join #t as T on T.ord = C.ord + 1
)
select C.ord, C.total, C.running_total
from CTE_RunningTotal as C
option (maxrecursion 0)

-- CPU 140, Reads 110014, Duration 132

sql fiddle demo

Update Ich war auch neugierig auf dieses Update mit variablem oder skurrilem Update . Normalerweise funktioniert es also in Ordnung, aber wie können wir sicher sein, dass es jedes Mal funktioniert? Nun, hier ist ein kleiner Trick (hier zu finden - http://www.sqlservercentral.com/Forums/Topic802558-203-21.aspx#bm981258 ) - Sie überprüfen einfach die aktuelle und vorherige ordund verwenden die 1/0Zuordnung, falls sie sich von der unterscheidet Sie erwarten:

declare @total int, @ord int

select @total = 0, @ord = -1

update #t set
    @total = @total + total,
    @ord = case when ord <> @ord + 1 then 1/0 else ord end,
    ------------------------
    running_total = @total

select * from #t

-- CPU 0, Reads 58, Duration 139

Nach dem, was ich gesehen habe, wenn Sie einen richtigen Clustered-Index / Primärschlüssel in Ihrer Tabelle haben (in unserem Fall wäre es ein Index nach ord_id), wird die Aktualisierung die ganze Zeit linear durchgeführt (nie angetroffen durch Null teilen). Das heißt, es liegt an Ihnen zu entscheiden, ob Sie es im Produktionscode verwenden möchten :)

Update 2 Ich verknüpfe diese Antwort, da sie einige nützliche Informationen zur Unzuverlässigkeit des skurrilen Updates enthält - nvarchar Verkettung / Index / nvarchar (max) unerklärliches Verhalten .


6
Diese Antwort verdient mehr Anerkennung (oder hat sie vielleicht einen Fehler, den ich nicht sehe?)
user1068352

Es sollte eine fortlaufende Nummer geben, damit Sie sich ord = ord + 1 anschließen können, und manchmal ist etwas mehr Arbeit erforderlich. Aber auf SQL 2008 R2 verwende ich diese Lösung
Roman Pekar

+1 Auf SQLServer2008R2 bevorzuge ich auch einen Ansatz mit rekursivem CTE. Zu Ihrer Information, um den Wert für die Tabellen zu finden, die Lücken zulassen, verwende ich eine korrelierte Unterabfrage. Es fügt der Abfrage sqlfiddle.com/#!3/d41d8/18967
Aleksandr Fedorenko

2
Für den Fall, dass Sie bereits eine Ordnungszahl für Ihre Daten haben und nach einer prägnanten (nicht Cursor) satzbasierten Lösung für SQL 2008 R2 suchen, scheint dies perfekt zu sein.
Nick.McDermaid

1
Nicht jede laufende Gesamtabfrage verfügt über ein zusammenhängendes Ordnungsfeld. Manchmal haben Sie ein Datum / Uhrzeit-Feld oder Datensätze wurden aus der Mitte der Sortierung gelöscht. Das könnte der Grund sein, warum es nicht öfter verwendet wird.
Reuben

28

Der APPLY-Operator in SQL 2005 und höher funktioniert dafür:

select
    t.id ,
    t.somedate ,
    t.somevalue ,
    rt.runningTotal
from TestTable t
 cross apply (select sum(somevalue) as runningTotal
                from TestTable
                where somedate <= t.somedate
            ) as rt
order by t.somedate

5
Funktioniert sehr gut für kleinere Datensätze. Ein Nachteil ist, dass Sie identische where-Klauseln für die innere und äußere Abfrage haben müssen.
Sire

Da einige meiner Daten genau gleich waren (bis auf den Bruchteil einer Sekunde), musste ich: row_number () over (Reihenfolge nach txndate) zur inneren und äußeren Tabelle und einige zusammengesetzte Indizes hinzufügen, damit sie ausgeführt werden. Glatte / einfache Lösung. Übrigens, getestetes Kreuz gilt für Unterabfragen ... es ist etwas schneller.
pghcpa

Dies ist sehr sauber und funktioniert gut mit kleinen Datenmengen. schneller als der rekursive CTE
jtate

Dies ist auch eine gute Lösung (für kleine Datenmengen), aber Sie müssen sich auch darüber im Klaren sein, dass eine bestimmte Spalte eindeutig sein muss
Roman Pekar

11
SELECT TOP 25   amount, 
    (SELECT SUM(amount) 
    FROM time_detail b 
    WHERE b.time_detail_id <= a.time_detail_id) AS Total FROM time_detail a

Sie können auch die Funktion ROW_NUMBER () und eine temporäre Tabelle verwenden, um eine beliebige Spalte zu erstellen, die für den Vergleich der inneren SELECT-Anweisung verwendet werden soll.


1
Das ist wirklich ineffizient ... aber andererseits gibt es keine wirklich saubere Möglichkeit, dies in SQL Server zu tun
Sam Saffron

Absolut ineffizient - aber es macht den Job und es steht außer Frage, ob etwas in der richtigen oder falschen Reihenfolge ausgeführt wird.
Sam Axe

danke, es ist nützlich, alternative Antworten zu haben, und auch nützlich, um eine effiziente Kritik zu haben
codeulike

7

Verwenden Sie eine korrelierte Unterabfrage. Sehr einfach, los geht's:

SELECT 
somedate, 
(SELECT SUM(somevalue) FROM TestTable t2 WHERE t2.somedate<=t1.somedate) AS running_total
FROM TestTable t1
GROUP BY somedate
ORDER BY somedate

Der Code ist möglicherweise nicht genau korrekt, aber ich bin mir sicher, dass die Idee so ist.

Wenn ein Datum mehrmals angezeigt wird, möchten Sie es in der Ergebnismenge nur einmal sehen.

Wenn es Ihnen nichts ausmacht, sich wiederholende Daten zu sehen, oder wenn Sie den ursprünglichen Wert und die ID sehen möchten, möchten Sie Folgendes:

SELECT 
id,
somedate, 
somevalue,
(SELECT SUM(somevalue) FROM TestTable t2 WHERE t2.somedate<=t1.somedate) AS running_total
FROM TestTable t1
ORDER BY somedate

Danke ... einfach war super. Es gab einen Index, der für die Leistung hinzugefügt werden musste, aber der war einfach genug (gemäß einer der Empfehlungen von Database Engine Tuning Advisor;), und dann lief er wie ein Schuss.
Doug_Ivison


4

Angenommen, die Fensterung funktioniert unter SQL Server 2008 wie anderswo (was ich versucht habe), probieren Sie Folgendes aus:

select testtable.*, sum(somevalue) over(order by somedate)
from testtable
order by somedate;

MSDN sagt, dass es in SQL Server 2008 (und vielleicht auch 2005?) Verfügbar ist, aber ich habe keine Instanz zur Hand, um es zu versuchen.

BEARBEITEN: Nun, anscheinend erlaubt SQL Server keine Fensterspezifikation ("OVER (...)") ohne Angabe von "PARTITION BY" (Aufteilung des Ergebnisses in Gruppen, aber nicht so aggregieren wie GROUP BY). Ärgerlich - die MSDN-Syntaxreferenz legt nahe, dass dies optional ist, aber ich habe derzeit nur SqlServer 2000-Instanzen.

Die Abfrage, die ich gestellt habe, funktioniert sowohl in Oracle 10.2.0.3.0 als auch in PostgreSQL 8.4-beta. Also sag MS, sie soll aufholen;)


2
Die Verwendung von OVER mit SUM funktioniert in diesem Fall nicht, um eine laufende Summe zu erhalten. Die OVER-Klausel akzeptiert ORDER BY nicht, wenn sie mit SUM verwendet wird. Sie müssen PARTITION BY verwenden, was für die Ausführung von Summen nicht funktioniert.
Sam Axe

Danke, es ist wirklich nützlich zu hören, warum dies nicht funktioniert. araqnid vielleicht könnten Sie Ihre Antwort bearbeiten, um zu erklären, warum es keine Option ist
codeulike


Dies funktioniert tatsächlich für mich, da ich partitionieren muss. Obwohl dies nicht die beliebteste Antwort ist, ist es die einfachste Lösung für mein Problem für RT in SQL.
William MB

Ich habe MSSQL 2008 nicht bei mir, aber ich denke, Sie könnten wahrscheinlich durch (wählen Sie null) partitionieren und das Partitionierungsproblem umgehen. Oder machen Sie eine Unterauswahl mit 1 partitionmeund partitionieren Sie danach. Außerdem ist in realen Situationen bei der Erstellung von Berichten wahrscheinlich eine Partitionierung nach erforderlich.
Nurettin

4

Wenn Sie den oben genannten SQL Server 2008 R2 verwenden. Dann wäre es der kürzeste Weg;

Select id
    ,somedate
    ,somevalue,
LAG(runningtotal) OVER (ORDER BY somedate) + somevalue AS runningtotal
From TestTable 

LAG wird verwendet, um den vorherigen Zeilenwert abzurufen. Sie können Google für weitere Informationen tun.

[1]:


1
Ich glaube, LAG existiert nur in SQL Server 2012 und höher (nicht 2008)
AaA

1
Die Verwendung von LAG () verbessert sich nicht, SUM(somevalue) OVER(...) was mir viel sauberer erscheint
Used_By_Already

2

Ich glaube, dass mit der folgenden einfachen INNER JOIN-Operation eine laufende Summe erreicht werden kann.

SELECT
     ROW_NUMBER() OVER (ORDER BY SomeDate) AS OrderID
    ,rt.*
INTO
    #tmp
FROM
    (
        SELECT 45 AS ID, CAST('01-01-2009' AS DATETIME) AS SomeDate, 3 AS SomeValue
        UNION ALL
        SELECT 23, CAST('01-08-2009' AS DATETIME), 5
        UNION ALL
        SELECT 12, CAST('02-02-2009' AS DATETIME), 0
        UNION ALL
        SELECT 77, CAST('02-14-2009' AS DATETIME), 7
        UNION ALL
        SELECT 39, CAST('02-20-2009' AS DATETIME), 34
        UNION ALL
        SELECT 33, CAST('03-02-2009' AS DATETIME), 6
    ) rt

SELECT
     t1.ID
    ,t1.SomeDate
    ,t1.SomeValue
    ,SUM(t2.SomeValue) AS RunningTotal
FROM
    #tmp t1
    JOIN #tmp t2
        ON t2.OrderID <= t1.OrderID
GROUP BY
     t1.OrderID
    ,t1.ID
    ,t1.SomeDate
    ,t1.SomeValue
ORDER BY
    t1.OrderID

DROP TABLE #tmp

Ja, ich denke, dies entspricht 'Test 3' in Sam Saffrons Antwort.
Codeulike

2

Im Folgenden werden die erforderlichen Ergebnisse erzielt.

SELECT a.SomeDate,
       a.SomeValue,
       SUM(b.SomeValue) AS RunningTotal
FROM TestTable a
CROSS JOIN TestTable b
WHERE (b.SomeDate <= a.SomeDate) 
GROUP BY a.SomeDate,a.SomeValue
ORDER BY a.SomeDate,a.SomeValue

Ein Clustered-Index für SomeDate verbessert die Leistung erheblich.


@ Dave Ich denke, diese Frage versucht, einen effizienten Weg zu finden, um dies zu tun. Cross Joining wird für große Mengen sehr langsam sein
Sam Saffron

danke, es ist nützlich, alternative Antworten zu haben, und auch nützlich, um eine effiziente Kritik zu haben
codeulike


2

Der beste Weg, dies zu erreichen, ist die Verwendung einer Fensterfunktion. Sie kann jedoch auch eine einfache korrelierte Unterabfrage verwenden .

Select id, someday, somevalue, (select sum(somevalue) 
                                from testtable as t2
                                where t2.id = t1.id
                                and t2.someday <= t1.someday) as runningtotal
from testtable as t1
order by id,someday;

0
BEGIN TRAN
CREATE TABLE #Table (_Id INT IDENTITY(1,1) ,id INT ,    somedate VARCHAR(100) , somevalue INT)


INSERT INTO #Table ( id  ,    somedate  , somevalue  )
SELECT 45 , '01/Jan/09', 3 UNION ALL
SELECT 23 , '08/Jan/09', 5 UNION ALL
SELECT 12 , '02/Feb/09', 0 UNION ALL
SELECT 77 , '14/Feb/09', 7 UNION ALL
SELECT 39 , '20/Feb/09', 34 UNION ALL
SELECT 33 , '02/Mar/09', 6 

;WITH CTE ( _Id, id  ,  _somedate  , _somevalue ,_totvalue ) AS
(

 SELECT _Id , id  ,    somedate  , somevalue ,somevalue
 FROM #Table WHERE _id = 1
 UNION ALL
 SELECT #Table._Id , #Table.id  , somedate  , somevalue , somevalue + _totvalue
 FROM #Table,CTE 
 WHERE #Table._id > 1 AND CTE._Id = ( #Table._id-1 )
)

SELECT * FROM CTE

ROLLBACK TRAN

Sie sollten wahrscheinlich einige Informationen darüber geben, was Sie hier tun, und alle Vor- und Nachteile dieser bestimmten Methode beachten.
TT.

0

Hier sind 2 einfache Methoden zur Berechnung der laufenden Summe:

Ansatz 1 : Es kann so geschrieben werden, wenn Ihr DBMS analytische Funktionen unterstützt

SELECT     id
           ,somedate
           ,somevalue
           ,runningtotal = SUM(somevalue) OVER (ORDER BY somedate ASC)
FROM       TestTable

Ansatz 2 : Sie können OUTER APPLY verwenden, wenn Ihre Datenbankversion / DBMS selbst keine Analysefunktionen unterstützt

SELECT     T.id
           ,T.somedate
           ,T.somevalue
           ,runningtotal = OA.runningtotal
FROM       TestTable T
           OUTER APPLY (
                           SELECT   runningtotal = SUM(TI.somevalue)
                           FROM     TestTable TI
                           WHERE    TI.somedate <= S.somedate
                       ) OA;

Hinweis: - Wenn Sie die laufende Summe für verschiedene Partitionen separat berechnen müssen, können Sie wie hier beschrieben vorgehen: Berechnung der laufenden Summe über Zeilen hinweg und Gruppierung nach ID

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.