Hitung Total yang Berjalan di SQL Server

170

Bayangkan tabel berikut (disebut 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

Saya ingin kueri yang mengembalikan total berjalan dalam urutan tanggal, seperti:

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

Saya tahu ada berbagai cara untuk melakukan ini di SQL Server 2000/2005/2008.

Saya sangat tertarik dengan metode semacam ini yang menggunakan trik pernyataan-agregat-set-pernyataan:

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

... ini sangat efisien tetapi saya telah mendengar ada masalah di sekitar ini karena Anda tidak dapat selalu menjamin bahwa UPDATEpernyataan akan memproses baris dalam urutan yang benar. Mungkin kita bisa mendapatkan jawaban yang pasti tentang masalah itu.

Tapi mungkin ada cara lain yang bisa disarankan orang?

sunting: Sekarang dengan SqlFiddle dengan setup dan contoh 'trik pembaruan' di atas

seperti kode
sumber
blogs.msdn.com/sqltips/archive/2005/07/20/441053.aspx Tambahkan pesanan hingga ke pembaruan Anda ... atur dan Anda mendapatkan jaminan.
Simon D
Tetapi Pesanan oleh tidak dapat diterapkan pada pernyataan PEMBARUAN ... bukan?
codeulike
Lihat juga sqlperformance.com/2012/07/t-sql-queries/running-totals terutama jika Anda menggunakan SQL Server 2012.
Aaron Bertrand

Jawaban:

133

Pembaruan , jika Anda menjalankan SQL Server 2012 lihat: https://stackoverflow.com/a/10309947

Masalahnya adalah bahwa implementasi SQL Server dari klausa Over agak terbatas .

Oracle (dan ANSI-SQL) memungkinkan Anda melakukan hal-hal seperti:

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

SQL Server tidak memberi Anda solusi bersih untuk masalah ini. Perasaan saya mengatakan kepada saya bahwa ini adalah salah satu kasus yang jarang terjadi di mana kursor adalah yang tercepat, meskipun saya harus melakukan pembandingan pada hasil yang besar.

Trik pembaruan berguna tetapi saya merasa ini cukup rapuh. Tampaknya jika Anda memperbarui tabel lengkap maka itu akan melanjutkan dalam urutan kunci utama. Jadi, jika Anda menetapkan tanggal sebagai kunci primer naik, Anda akan probablyaman. Tetapi Anda mengandalkan detail implementasi SQL Server tidak berdokumen (juga jika kueri akhirnya dilakukan oleh dua procs. Saya ingin tahu apa yang akan terjadi, lihat: MAXDOP):

Sampel kerja penuh:

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

Anda meminta patokan, ini adalah lowdown.

Cara AMAN tercepat untuk melakukan ini adalah kursor, ini adalah urutan besarnya lebih cepat dari sub-kueri berkorelasi dengan cross-join.

Cara tercepat mutlak adalah trik UPDATE. Satu-satunya kekhawatiran saya adalah bahwa saya tidak yakin bahwa dalam semua keadaan pembaruan akan diproses secara linear. Tidak ada dalam kueri yang secara eksplisit mengatakan demikian.

Intinya, untuk kode produksi saya akan pergi dengan kursor.

Data uji:

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

Tes 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 

Tes 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

Tes 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

Tes 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
Sam Saffron
sumber
1
Terima kasih. Jadi contoh kode Anda adalah untuk menunjukkan bahwa itu akan dijumlahkan dalam urutan kunci utama, saya kira. Akan menarik untuk mengetahui apakah kursor masih lebih efisien daripada gabungan untuk set data yang lebih besar.
codeulike
1
Saya baru saja menguji CTE @ Martin, tidak ada yang mendekati trik pembaruan - kursor tampaknya lebih rendah saat dibaca. Berikut adalah jejak profiler i.stack.imgur.com/BbZq3.png
Sam Saffron
3
@ Martin Denali akan memiliki solusi yang cukup bagus untuk msdn.microsoft.com/en-us/library/ms189461(v=SQL.110).aspx
Sam Saffron
1
+1 untuk semua pekerjaan yang dimasukkan ke dalam jawaban ini - Saya suka opsi UPDATE; dapatkah partisi dibangun ke dalam skrip UPDATE ini? mis. jika ada bidang tambahan "Warna Mobil" dapatkah skrip ini mengembalikan total yang berjalan dalam setiap partisi "Warna Mobil"?
whytheq
2
jawaban awal (Oracle (dan ANSI-SQL)) sekarang berfungsi di SQL server 2017. Terima kasih, sangat elegan!
DaniDev
121

Di SQL Server 2012 Anda bisa menggunakan SUM () dengan klausa OVER () .

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

SQL Fiddle

Mikael Eriksson
sumber
40

Sementara Sam Saffron melakukan pekerjaan besar, ia masih tidak menyediakan kode ekspresi tabel rekursif umum untuk masalah ini. Dan bagi kami yang bekerja dengan SQL Server 2008 R2 dan bukan Denali, ini masih cara tercepat untuk menjalankan total, ini sekitar 10 kali lebih cepat daripada kursor di komputer kerja saya untuk 100000 baris, dan juga inline query.
Jadi, ini dia (saya kira ada ordkolom di tabel dan nomor berurutan tanpa celah, untuk pemrosesan cepat juga harus ada batasan unik pada nomor ini):

;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

pembaruan Saya juga ingin tahu tentang pembaruan ini dengan pembaruan variabel atau unik . Jadi biasanya itu berfungsi dengan baik, tetapi bagaimana kita bisa yakin itu bekerja setiap saat? baik, inilah sedikit trik (temukan di sini - http://www.sqlservercentral.com/Forums/Topic802558-203-21.aspx#bm981258 ) - Anda cukup memeriksa tugas saat ini dan sebelumnya orddan menggunakan 1/0jika ada perbedaan dari apa Anda mengharapkan:

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

Dari apa yang saya lihat jika Anda memiliki indeks / kunci utama berkerumun yang tepat di meja Anda (dalam kasus kami akan diindeks oleh ord_id) pembaruan akan diproses secara linear sepanjang waktu (tidak pernah dijumpai dengan angka nol). Yang mengatakan, itu terserah Anda untuk memutuskan apakah Anda ingin menggunakannya dalam kode produksi :)

pembaruan 2 Saya menautkan jawaban ini, karena ini mencakup beberapa info bermanfaat tentang tidak dapat diandalkannya pembaruan unik - nvarchar concatenation / index / nvarchar (max) perilaku yang tidak dapat dijelaskan .

Roman Pekar
sumber
6
Jawaban ini layak mendapat pengakuan lebih (atau mungkin memiliki beberapa cacat yang saya tidak melihat?)
user1068352
harus ada nomor urut sehingga Anda dapat bergabung di ord = ord + 1 dan kadang-kadang perlu sedikit lebih banyak pekerjaan. Tapi bagaimanapun, pada SQL 2008 R2 saya menggunakan solusi ini
Roman Pekar
+1 Pada SQLServer2008R2 Saya juga lebih suka pendekatan dengan CTE rekursif. FYI, untuk menemukan nilai untuk tabel, yang memungkinkan kesenjangan saya menggunakan sub-kueri berkorelasi. Ia menambahkan dua operasi pencarian tambahan ke permintaan sqlfiddle.com/#!3/d41d8/18967
Aleksandr Fedorenko
2
Untuk kasus di mana Anda sudah memiliki ordinal untuk data Anda dan Anda sedang mencari solusi berbasis set ringkas (non kursor) pada SQL 2008 R2, ini tampaknya sempurna.
Nick.McDermaid
1
Tidak setiap kueri total yang berjalan akan memiliki bidang ordinal yang berdekatan. Terkadang bidang datetime adalah apa yang Anda miliki, atau catatan telah dihapus dari tengah-tengah semacam itu. Itu mungkin mengapa itu tidak digunakan lebih sering.
Ruben
28

Operator BERLAKU di SQL 2005 dan yang lebih tinggi berfungsi untuk ini:

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
Mike Forman
sumber
5
Bekerja sangat baik untuk kumpulan data yang lebih kecil. Kelemahannya adalah Anda harus memiliki klausa tempat identik pada permintaan dalam dan luar.
Baginda
Karena beberapa teman kencan saya persis sama (hingga sepersekian detik) saya harus menambahkan: row_number () over (order by txndate) ke tabel dalam dan luar dan beberapa indeks gabungan untuk membuatnya berjalan. Solusi licin / sederhana. BTW, lintas yang diuji berlaku terhadap subquery ... sedikit lebih cepat.
pghcpa
ini sangat bersih dan bekerja dengan baik dengan set data kecil; lebih cepat daripada CTE rekursif
jtate
ini adalah solusi yang bagus juga (untuk set data kecil), tetapi Anda juga harus menyadari bahwa ini menyiratkan kolom tertentu untuk menjadi unik
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

Anda juga dapat menggunakan fungsi ROW_NUMBER () dan tabel temp untuk membuat kolom arbitrer untuk digunakan dalam perbandingan pada pernyataan SELECT bagian dalam.

Sam Axe
sumber
1
Ini benar-benar tidak efisien ... tapi sekali lagi tidak ada cara nyata yang bersih untuk melakukan ini di sql server
Sam Saffron
Benar-benar tidak efisien - tetapi ia bekerja dan tidak ada pertanyaan apakah sesuatu untuk dieksekusi dalam urutan yang benar atau salah.
Sam Axe
terima kasih, ini berguna untuk memiliki jawaban alternatif, dan juga berguna untuk memiliki kritik yang efisien
codeulike
7

Gunakan sub-kueri yang berkorelasi. Sangat sederhana, ini dia:

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

Kode mungkin tidak tepat benar, tapi saya yakin idenya benar.

GROUP BY jika seandainya tanggal muncul lebih dari sekali, Anda hanya ingin melihatnya sekali di set hasil.

Jika Anda tidak keberatan melihat tanggal berulang, atau Anda ingin melihat nilai dan id asli, maka berikut ini yang Anda inginkan:

SELECT 
id,
somedate, 
somevalue,
(SELECT SUM(somevalue) FROM TestTable t2 WHERE t2.somedate<=t1.somedate) AS running_total
FROM TestTable t1
ORDER BY somedate
KthProg
sumber
Terima kasih ... sederhana itu hebat. Ada indeks untuk menambah kinerja, tapi itu cukup sederhana, (mengambil salah satu rekomendasi dari Database Engine Tuning Advisor;), dan kemudian berjalan seperti suntikan.
Doug_Ivison
4

Dengan asumsi bahwa windowing berfungsi pada SQL Server 2008 seperti yang dilakukannya di tempat lain (yang telah saya coba), cobalah:

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

MSDN mengatakan itu tersedia di SQL Server 2008 (dan mungkin 2005 juga?) Tapi saya tidak punya contoh untuk mencobanya.

EDIT: well, rupanya SQL Server tidak mengizinkan spesifikasi jendela ("LEBIH (...)") tanpa menentukan "PARTITION BY" (membagi hasilnya menjadi beberapa kelompok tetapi tidak menggabungkan dengan cara yang dilakukan GROUP BY). Mengganggu - referensi sintaks MSDN menunjukkan bahwa itu opsional, tetapi saya hanya memiliki SqlServer 2000 contoh sekitar saat ini.

Kueri yang saya berikan bekerja di Oracle 10.2.0.3.0 dan PostgreSQL 8.4-beta. Jadi beritahu MS untuk mengejar ketinggalan;)

araqnid
sumber
2
Menggunakan OVER dengan SUM tidak akan berfungsi dalam hal ini untuk memberikan total yang berjalan. Klausa OVER tidak menerima ORDER BY saat digunakan dengan SUM. Anda harus menggunakan PARTITION BY, yang tidak akan berfungsi untuk menjalankan total.
Sam Axe
terima kasih, ini sebenarnya berguna untuk mendengar mengapa ini tidak berfungsi araqnid mungkin Anda bisa mengedit jawaban Anda untuk menjelaskan mengapa yang bukan pilihan
codeulike
Ini sebenarnya bekerja untuk saya, karena saya perlu mempartisi - jadi walaupun ini bukan jawaban yang paling populer, ini adalah solusi termudah untuk masalah saya untuk RT di SQL.
William MB
Saya tidak memiliki MSSQL 2008 dengan saya, tapi saya pikir Anda mungkin bisa mempartisi dengan (pilih null) dan meretas masalah pemartisian. Atau buat subselect dengan 1 partitionmedan partisi dengan itu. Selain itu, partisi oleh mungkin diperlukan dalam situasi kehidupan nyata ketika melakukan laporan.
nurettin
4

Jika Anda menggunakan Sql server 2008 R2 di atas. Maka, Ini akan menjadi cara terpendek untuk dilakukan;

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

LAG digunakan untuk mendapatkan nilai baris sebelumnya. Anda dapat melakukan google untuk info lebih lanjut.

[1]:

shambhu yadav
sumber
1
Saya percaya LAG hanya ada di SQL server 2012 dan di atasnya (bukan 2008)
AaA
1
Menggunakan LAG () tidak meningkatkan SUM(somevalue) OVER(...) yang tampaknya jauh lebih bersih bagi saya
Used_By_Already
2

Saya percaya total yang berjalan dapat dicapai dengan menggunakan operasi INNER JOIN sederhana di bawah ini.

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
clevster
sumber
Ya, saya pikir ini setara dengan 'Tes 3' dalam jawaban Sam Saffron.
kode pada
2

Berikut ini akan menghasilkan hasil yang diperlukan.

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

Memiliki indeks berkerumun di SomeDate akan sangat meningkatkan kinerja.

Dave Barker
sumber
@Dave Saya pikir pertanyaan ini sedang mencoba untuk menemukan cara yang efisien untuk melakukan hal ini, bergabung bersama akan sangat lambat untuk set besar
Sam Saffron
terima kasih, ini berguna untuk memiliki jawaban alternatif, dan juga berguna untuk memiliki kritik yang efisien
codeulike
2

Meskipun cara terbaik untuk menyelesaikannya adalah menggunakan fungsi jendela, itu juga dapat dilakukan dengan menggunakan sub-kueri berkorelasi sederhana .

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;
Krahul3
sumber
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
Mansur
sumber
Anda mungkin harus memberikan beberapa informasi tentang apa yang Anda lakukan di sini, dan catat setiap kelebihan / kekurangan metode khusus ini.
TT.
0

Berikut adalah 2 cara sederhana untuk menghitung jumlah running:

Pendekatan 1 : Dapat ditulis dengan cara ini jika DBMS Anda mendukung Fungsi Analitik

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

Pendekatan 2 : Anda dapat menggunakan OUTER APPLY jika versi database / DBMS Anda sendiri tidak mendukung Fungsi Analitik

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;

Catatan: - Jika Anda harus menghitung total running untuk partisi berbeda secara terpisah, itu dapat dilakukan seperti yang diposting di sini: Menghitung total running di seluruh baris dan pengelompokan berdasarkan ID

san
sumber