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

Berechnen Sie eine laufende Summe in SQL Server

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

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

Oracle (und ANSI-SQL) erlauben Ihnen Dinge wie:

 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 Bauchgefühl sagt mir, dass dies einer der seltenen Fälle ist, in denen ein Cursor am schnellsten ist, obwohl ich bei großen Ergebnissen ein Benchmarking durchführen muss.

Der Update-Trick ist praktisch, aber ich fühle mich ziemlich zerbrechlich. Es scheint, dass, wenn Sie eine vollständige Tabelle aktualisieren, diese in der Reihenfolge des Primärschlüssels fortfahren wird. Wenn Sie also Ihr Datum als Primärschlüssel aufsteigend setzen, werden Sie probably sicher sein. Aber Sie verlassen sich auf ein undokumentiertes SQL Server-Implementierungsdetail (auch wenn die Abfrage am Ende von zwei Procs ausgeführt wird, frage ich mich, was passieren wird, siehe:MAXDOP):

Voll funktionsfähiges Beispiel:

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, das sind die Fakten.

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

Der absolut schnellste Weg ist der UPDATE-Trick. Meine einzige Sorge dabei ist, dass ich nicht sicher bin, ob das Update unter allen Umständen linear ablaufen wird. Es gibt nichts in der Abfrage, die dies ausdrücklich sagt.

Unterm Strich würde ich für 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