Tính tổng chạy trong SQL Server


169

Hãy tưởng tượng bảng sau (được gọi 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

Tôi muốn một truy vấn trả về tổng số đang chạy theo thứ tự ngày, như:

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

Tôi biết có nhiều cách khác nhau để làm điều này trong SQL Server 2000/2005/2008.

Tôi đặc biệt quan tâm đến loại phương pháp này sử dụng thủ thuật tổng hợp-set-statement:

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

... Điều này rất hiệu quả nhưng tôi đã nghe nói có những vấn đề xung quanh vấn đề này bởi vì bạn không nhất thiết phải đảm bảo rằng UPDATEcâu lệnh sẽ xử lý các hàng theo đúng thứ tự. Có lẽ chúng ta có thể nhận được một số câu trả lời dứt khoát về vấn đề đó.

Nhưng có lẽ có những cách khác mà mọi người có thể đề xuất?

chỉnh sửa: Bây giờ với một SqlFiddle với thiết lập và ví dụ 'mẹo cập nhật' ở trên


blog.msdn.com/sqltips/archive/2005/07/20/441053.aspx Thêm một đơn đặt hàng vào bản cập nhật của bạn ... được đặt và bạn nhận được một sự đảm bảo.
Simon D

Nhưng Order by không thể được áp dụng cho một câu lệnh CẬP NHẬT ... có thể không?
codeulike

Đồng thời xem sqlperformance.com/2012/07/t-sql-queries/rucky-totals đặc biệt nếu bạn đang sử dụng SQL Server 2012.
Aaron Bertrand

Câu trả lời:


133

Cập nhật , nếu bạn đang chạy SQL Server 2012, hãy xem: https://stackoverflow.com/a/10309947

Vấn đề là việc triển khai SQL Server của mệnh đề Over có phần hạn chế .

Oracle (và ANSI-SQL) cho phép bạn làm những việc như:

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

SQL Server cung cấp cho bạn không có giải pháp rõ ràng cho vấn đề này. Chú ruột của tôi đang nói với tôi rằng đây là một trong những trường hợp hiếm hoi mà con trỏ là nhanh nhất, mặc dù tôi sẽ phải thực hiện một số điểm chuẩn cho kết quả lớn.

Thủ thuật cập nhật rất tiện dụng nhưng tôi cảm thấy nó khá dễ vỡ. Có vẻ như nếu bạn đang cập nhật một bảng đầy đủ thì nó sẽ tiến hành theo thứ tự của khóa chính. Vì vậy, nếu bạn đặt ngày của bạn làm khóa chính tăng dần, bạn sẽ probablyan toàn. Nhưng bạn đang dựa vào một chi tiết triển khai SQL Server không có giấy tờ (cũng như nếu truy vấn kết thúc được thực hiện bởi hai procs tôi tự hỏi điều gì sẽ xảy ra, xem: MAXDOP):

Mẫu làm việc đầy đủ:

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

Bạn đã yêu cầu một điểm chuẩn đây là mức thấp.

Cách SAFE nhanh nhất để làm điều này sẽ là Con trỏ, nó là một thứ tự có độ lớn nhanh hơn truy vấn phụ tương quan của liên kết chéo.

Cách nhanh nhất tuyệt đối là thủ thuật CẬP NHẬT. Mối quan tâm duy nhất của tôi với nó là tôi không chắc chắn rằng trong mọi trường hợp, việc cập nhật sẽ tiến hành theo cách tuyến tính. Không có gì trong truy vấn nói rõ ràng như vậy.

Dòng dưới cùng, đối với mã sản xuất tôi sẽ đi với con trỏ.

Dữ liệu kiểm tra:

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

Kiểm tra 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 

Bài kiểm tra 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

Bài kiểm tra 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

Bài kiểm tra 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
Cảm ơn. Vì vậy, mẫu mã của bạn là để chứng minh rằng nó sẽ tổng hợp theo thứ tự của khóa chính, tôi đoán vậy. Sẽ rất thú vị nếu biết các con trỏ vẫn hiệu quả hơn so với tham gia cho các tập dữ liệu lớn hơn.
codeulike

1
Tôi vừa thử nghiệm CTE @Martin, không có gì gần với thủ thuật cập nhật - con trỏ có vẻ thấp hơn khi đọc. Đây là một dấu vết hồ sơ i.stack.imgur.com/BbZq3.png
Sam Saffron

3
@ Martin Denali sẽ có một giải pháp tốt đẹp khá cho điều này msdn.microsoft.com/en-us/library/ms189461(v=SQL.110).aspx
Sam Saffron

1
+1 cho tất cả các công việc đưa vào câu trả lời này - Tôi thích tùy chọn CẬP NHẬT; một phân vùng có thể được xây dựng trong kịch bản CẬP NHẬT này không? ví dụ: nếu có thêm một trường "Màu xe" thì tập lệnh này có thể trả về tổng số đang chạy trong mỗi phân vùng "Màu xe" không?
whytheq

2
câu trả lời ban đầu (Oracle (và ANSI-SQL)) hiện hoạt động trong máy chủ SQL 2017. Cảm ơn bạn, rất thanh lịch!
DaniDev

121

Trong SQL Server 2012, bạn có thể sử dụng SUM () với mệnh đề OVER () .

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

Câu đố SQL


40

Trong khi Sam Saffron đã làm rất tốt với nó, anh ta vẫn không cung cấp mã biểu thức bảng chung đệ quy cho vấn đề này. Và đối với chúng tôi, những người làm việc với SQL Server 2008 R2 chứ không phải Denali, đó vẫn là cách nhanh nhất để chạy tổng cộng, nó nhanh hơn khoảng 10 lần so với con trỏ trên máy tính làm việc của tôi cho 100000 hàng và đó cũng là truy vấn nội tuyến.
Vì vậy, đây là (tôi cho rằng có một ordcột trong bảng và đó là số liên tiếp không có khoảng trống, để xử lý nhanh cũng cần có một ràng buộc duy nhất đối với số này):

;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

cập nhật Tôi cũng tò mò về bản cập nhật này với bản cập nhật biến hoặc kỳ quặc . Vì vậy, thường thì nó hoạt động tốt, nhưng làm thế nào chúng ta có thể chắc chắn rằng nó hoạt động mọi lúc? tốt, đây là một mẹo nhỏ (tìm thấy ở đây - http://www.sqlservercentral.com/Forums/Topic802558-203-21.aspx#bm981258 ) - bạn chỉ cần kiểm tra hiện tại và trước đó ordvà sử dụng 1/0bài tập trong trường hợp chúng khác với những gì bạn mong đợi:

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

Từ những gì tôi đã thấy nếu bạn có chỉ mục / khóa chính được phân cụm thích hợp trên bảng của bạn (trong trường hợp của chúng tôi, đó sẽ là chỉ mục theo ord_id) cập nhật sẽ tiến hành theo cách tuyến tính mọi lúc (không bao giờ gặp phải chia cho 0). Điều đó nói rằng, tùy thuộc vào bạn để quyết định nếu bạn muốn sử dụng nó trong mã sản xuất :)

update 2 Tôi đang liên kết câu trả lời này, vì nó bao gồm một số thông tin hữu ích về sự không đáng tin cậy của bản cập nhật kỳ quặc - hành vi không thể giải thích được của nvarchar / index / nvarchar (max) .


6
Câu trả lời này xứng đáng được công nhận nhiều hơn (hoặc có thể nó có một số lỗ hổng mà tôi không thấy?)
user1068352

cần có một số liên tiếp để bạn có thể tham gia vào ord = ord + 1 và đôi khi nó cần thêm một chút công việc. Nhưng dù sao, trên SQL 2008 R2 tôi đang sử dụng giải pháp này
Roman Pekar

+1 Trên SQLServer2008R2 tôi cũng thích cách tiếp cận với CTE đệ quy. FYI, để tìm giá trị cho các bảng, cho phép các khoảng trống tôi sử dụng truy vấn phụ tương quan. Nó bổ sung thêm hai thao tác tìm kiếm bổ sung cho truy vấn sqlfiddle.com/#!3/d41d8/18967
Aleksandr Fedorenko

2
Đối với trường hợp bạn đã có một thứ tự cho dữ liệu của mình và bạn đang tìm giải pháp dựa trên bộ súc tích (không phải con trỏ) trên SQL 2008 R2, thì điều này có vẻ hoàn hảo.
Nick.McDilyn

1
Không phải mọi truy vấn tổng thể đang chạy sẽ có một trường thứ tự tiếp giáp nhau. Đôi khi một trường datetime là những gì bạn có hoặc các bản ghi đã bị xóa từ giữa loại. Đó có thể là lý do tại sao nó không được sử dụng thường xuyên hơn.
Reuben

28

Toán tử ỨNG DỤNG trong SQL 2005 và cao hơn hoạt động cho việc này:

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
Hoạt động rất tốt cho các bộ dữ liệu nhỏ hơn. Một nhược điểm là bạn sẽ phải có các mệnh đề trong truy vấn bên trong và bên ngoài.
Sire

Vì một số ngày của tôi giống hệt nhau (giảm đến một phần của giây), tôi đã phải thêm: row_number () qua (thứ tự bởi txndate) vào bảng bên trong và bên ngoài và một vài chỉ số ghép để chạy. Giải pháp khéo léo / đơn giản. BTW, thử nghiệm chéo áp dụng chống lại truy vấn phụ ... nó nhanh hơn một chút.
pghcpa

Điều này rất sạch sẽ và hoạt động tốt với các tập dữ liệu nhỏ; nhanh hơn CTE đệ quy
jtate

đây cũng là một giải pháp tốt (đối với các tập dữ liệu nhỏ), nhưng bạn cũng phải lưu ý rằng nó ngụ ý cột một ngày nào đó là duy nhất
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

Bạn cũng có thể sử dụng hàm ROW_NUMBER () và bảng tạm thời để tạo một cột tùy ý để sử dụng trong so sánh trên câu lệnh SELECT bên trong.


1
Điều này thực sự không hiệu quả ... nhưng một lần nữa, không có cách nào thực sự sạch trong việc thực hiện điều này trong máy chủ sql
Sam Saffron

Hoàn toàn không hiệu quả - nhưng nó thực hiện công việc và không có câu hỏi liệu một cái gì đó được thực hiện theo thứ tự đúng hay sai.
Sam Axe

cảm ơn, thật hữu ích khi có câu trả lời thay thế và cũng hữu ích để có bài phê bình hiệu quả
codeulike

7

Sử dụng một truy vấn phụ tương quan. Rất đơn giản, ở đây bạn đi:

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

Mã có thể không chính xác, nhưng tôi chắc chắn rằng ý tưởng đó là.

NHÓM THEO trong trường hợp một ngày xuất hiện nhiều hơn một lần, bạn sẽ chỉ muốn nhìn thấy nó một lần trong tập kết quả.

Nếu bạn không thấy ngày lặp lại hoặc bạn muốn xem giá trị ban đầu và id, thì sau đây là những gì bạn muốn:

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

Cảm ơn ... đơn giản là tuyệt vời. Có một chỉ mục để thêm hiệu năng, nhưng điều đó đủ đơn giản, (lấy một trong các khuyến nghị từ Cố vấn điều chỉnh động cơ cơ sở dữ liệu;), và sau đó nó chạy như một phát súng.
Doug_Ivison


4

Giả sử rằng cửa sổ hoạt động trên SQL Server 2008 giống như ở nơi khác (mà tôi đã thử), hãy thử xem:

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

MSDN cho biết nó có sẵn trong SQL Server 2008 (và có thể cả năm 2005 nữa không?) Nhưng tôi không có ví dụ để dùng thử.

EDIT: tốt, rõ ràng SQL Server không cho phép một đặc tả cửa sổ ("QUÁ (...)") mà không chỉ định "PHẦN THAM GIA" (chia kết quả thành các nhóm nhưng không tổng hợp theo cách mà GROUP BY thực hiện). Làm phiền-- tham chiếu cú ​​pháp MSDN cho thấy tùy chọn của nó, nhưng tôi chỉ có các phiên bản SqlServer 2000 xung quanh vào lúc này.

Truy vấn tôi đã đưa ra hoạt động trong cả Oracle 10.2.0.3.0 và PostgreSQL 8.4-beta. Vì vậy, hãy nói với MS để bắt kịp;)


2
Sử dụng OVER với SUM sẽ không hoạt động trong trường hợp này để cung cấp tổng số hoạt động. Mệnh đề OVER không chấp nhận ORDER BY khi được sử dụng với SUM. Bạn phải sử dụng PHẦN THAM GIA, sẽ không hoạt động để chạy tổng số.
Sam Axe

cảm ơn, nó thực sự hữu ích để nghe lý do tại sao điều này sẽ không hoạt động. araqnid có lẽ bạn có thể chỉnh sửa câu trả lời của mình để giải thích lý do tại sao nó không phải là một tùy chọn
codeulike


Điều này thực sự hiệu quả với tôi, vì tôi cần phân vùng - vì vậy mặc dù đây không phải là câu trả lời phổ biến nhất, nhưng đây là giải pháp dễ nhất cho vấn đề của tôi đối với RT trong SQL.
William MB

Tôi không có MSSQL 2008 với tôi, nhưng tôi nghĩ bạn có thể phân vùng bằng cách (chọn null) và hack xung quanh vấn đề phân vùng. Hoặc tạo một mục phụ với 1 partitionmevà phân vùng bằng cách đó. Ngoài ra, phân vùng có lẽ là cần thiết trong các tình huống thực tế khi làm báo cáo.
Nurettin

4

Nếu bạn đang sử dụng máy chủ Sql 2008 R2 ở trên. Sau đó, nó sẽ là cách ngắn nhất để làm;

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

LAG được sử dụng để có được giá trị hàng trước đó. Bạn có thể làm google để biết thêm.

[1]:


1
Tôi tin rằng LAG chỉ tồn tại trong máy chủ SQL 2012 trở lên (không phải 2008)
AaA

1
Sử dụng LAG () không cải thiện đối SUM(somevalue) OVER(...) với tôi dường như sạch hơn rất nhiều
Được sử dụng_By_Al đã

2

Tôi tin rằng có thể đạt được tổng số hoạt động bằng cách sử dụng thao tác INNER THAM GIA đơn giản dưới đây.

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

Có, tôi nghĩ rằng điều này tương đương với 'Bài kiểm tra 3' trong câu trả lời của Sam Saffron.
codeulike

2

Sau đây sẽ tạo ra kết quả cần thiết.

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

Có một chỉ mục được nhóm trên một sốDate sẽ cải thiện hiệu suất rất nhiều.


@ Tôi nghĩ rằng câu hỏi này đang cố gắng tìm ra một cách hiệu quả để làm điều này, việc tham gia chéo sẽ rất chậm đối với các bộ lớn
Sam Saffron

cảm ơn, thật hữu ích khi có câu trả lời thay thế và cũng hữu ích để có bài phê bình hiệu quả
codeulike


2

Mặc dù cách tốt nhất là hoàn thành nó bằng cách sử dụng chức năng cửa sổ, nhưng cũng có thể được thực hiện bằng cách sử dụng truy vấn phụ tương quan đơn giản .

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

Bạn có thể nên cung cấp một số thông tin về những gì bạn đang làm ở đây, và lưu ý bất kỳ ưu điểm / nhược điểm của phương pháp cụ thể này.
TT.

0

Dưới đây là 2 cách đơn giản để tính tổng chạy:

Cách tiếp cận 1 : Nó có thể được viết theo cách này nếu DBMS của bạn hỗ trợ các chức năng phân tích

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

Cách tiếp cận 2 : Bạn có thể sử dụng OUTER ỨNG DỤNG nếu phiên bản cơ sở dữ liệu / DBMS của bạn không hỗ trợ Chức năng phân tích

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;

Lưu ý: - Nếu bạn phải tính riêng tổng số chạy cho các phân vùng khác nhau, thì có thể thực hiện như đã đăng ở đây: Tính tổng số Chạy trên các hàng và nhóm theo ID

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.