计算SQL Server中的运行总计


170

想象一下下表(称为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

我想要一个按日期顺序返回运行总计的查询,例如:

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

我知道在SQL Server 2000/2005/2008中有多种方法可以做到这一点

我对使用aggregating-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

...这非常有效,但是我听说周围存在问题,因为您不一定能保证该UPDATE语句将按正确的顺序处理行。也许我们可以获得有关该问题的明确答案。

但是,人们还有其他建议的方式吗?

编辑:现在使用带有上面的设置和“更新技巧”示例的SqlFiddle


blogs.msdn.com/sqltips/archive/2005/07/20/441053.aspx 在更新中添加订单...集,您将获得保证。
Simon D

但是Order by无法应用于UPDATE语句...可以吗?
codeulike

另请参阅sqlperformance.com/2012/07/t-sql-queries/running-totals,尤其是在使用SQL Server 2012的情况下。–
Aaron Bertrand

Answers:


133

更新,如果您正在运行SQL Server 2012,请参见:https : //stackoverflow.com/a/10309947

问题是Over子句的SQL Server实现受到一定的限制

Oracle(和ANSI-SQL)允许您执行以下操作:

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

SQL Server无法为您解决此问题。我的直觉告诉我,这是极少数情况下光标最快的情况之一,尽管我必须对大结果进行一些基准测试。

更新技巧很方便,但我觉得它相当脆弱。看来,如果要更新完整表,则它将按主键的顺序进行。因此,如果将日期设置为升序的主键,则将probably是安全的。但是您依赖于一个未记录的SQL Server实现细节(同样如果查询最终由两个proc执行,我想知道会发生什么,请参见:MAXDOP):

完整的工作样本:

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

您要求提供基准,这是最低点。

最快的SAFE方式是游标,它比交叉联接的相关子查询快一个数量级。

绝对最快的方法是UPDATE技巧。我唯一关心的是,我不确定在所有情况下更新都会以线性方式进行。查询中没有明确说明的内容。

底线,对于生产代码,我将使用光标。

测试数据:

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

测试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 

测试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

测试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

测试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
谢谢。因此,我想您的代码示例将演示它将按主键的顺序求和。知道游标是否仍然比连接较大数据集的连接更有效是很有趣的。
codeulike

1
我刚刚测试了CTE @Martin,没有什么比更新技巧更接近了-光标在读取时似乎更低。这是探查器跟踪i.stack.imgur.com/BbZq3.png
Sam Saffron

3
@马丁迪纳利是要对此有一个相当不错的解决方案msdn.microsoft.com/en-us/library/ms189461(v=SQL.110).aspx
萨姆藏红花

1
对于此答案中的所有工作,+ 1 –我喜欢UPDATE选项;分区可以内置到此UPDATE脚本中吗?例如,如果还有一个附加字段“ Car Colour”,此脚本可以返回每个“ Car Colour”分区内的运行总计吗?
whytheq 2012年

2
初始答案(Oracle(和ANSI-SQL))现在可以在SQL Server 2017中使用。谢谢,非常优雅!
DaniDev


40

尽管Sam Saffron在此方面做了出色的工作,但他仍然没有为该问题提供递归的公用表表达式代码。对于使用SQL Server 2008 R2而不是Denali的我们来说,这仍然是最快的总运行方式,它比我的工作计算机上的游标快了10倍,处理了100000行,它也是内联查询。
因此,这里就是(我假设表中有一ord列,并且它是无间隙的连续数字,为了快速处理,对该数字也应该有唯一的约束):

;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

更新 我也很好奇这个更新变量古怪的更新。因此通常它可以正常工作,但是我们如何确定每次都能正常工作呢?好吧,这是一个小技巧(在此处找到它-http: //www.sqlservercentral.com/Forums/Topic802558-203-21.aspx#bm981258)-您只需检查当前和以前的内容,ord并使用1/0分配以防它们与其他内容不同您期望:

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

从我所见,如果您的表上有正确的聚集索引/主键(在我们的情况下为index by ord_id),更新将一直以线性方式进行(从未遇到被零除)。就是说,由您决定是否要在生产代码中使用它:)

更新2我正在链接此答案,因为它包含一些有关古怪的更新的不可靠性的有用信息-nvarchar级联/索引/ nvarchar(max)莫名其妙的行为


6
这个答案值得更多的认可(或者也许它有一些我看不到的缺陷?)
user1068352 2013年

应该有一个序号,以便您可以在ord = ord + 1上加入,有时它需要做更多的工作。但是无论如何,在SQL 2008 R2上,我正在使用这种解决方案
Roman Pekar 2013年

+1在SQLServer2008R2上,我也更喜欢使用递归CTE的方法。仅供参考,为了找到表的值(允许出现空白),我使用了相关子查询。它向查询sqlfiddle.com/#!3/d41d8/18967
Aleksandr Fedorenko 2013年

2
对于已经有数据序数并且您正在寻找基于SQL 2008 R2的简洁(非游标)集解决方案的情况,这似乎是完美的。
Nick.McDermaid 2014年

1
并非每个正在运行的总计查询都具有连续的序数字段。有时日期时间字段就是您所拥有的,或者记录已从排序的中间删除。这可能就是为什么它不经常使用的原因。
Reuben 2014年

28

SQL 2005和更高版本中的APPLY运算符可用于此目的:

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
对于较小的数据集,效果很好。缺点是内部和外部查询上必须具有相同的where子句。
2012年

由于我的某些日期完全相同(不到一秒的时间),因此我必须在内部和外部表中添加:row_number()over(按txndate排序),并添加一些复合索引以使其运行。光滑/简单的解决方案。顺便说一句,经过测试的交叉适用于子查询...它稍微快一点。
pghcpa 2015年

这非常干净,并且在使用较小的数据集时效果很好;比递归CTE更快
-jtate

这也是一个不错的解决方案(对于小型数据集),但是您还必须意识到,这意味着somedate列是唯一的
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

您还可以使用ROW_NUMBER()函数和一个临时表来创建一个任意列,以用于内部SELECT语句的比较中。


1
这确实效率很低……但是,在SQL Server中再也没有真正的干净方法
Sam Saffron

绝对是没有效率的-但是它确实可以完成工作,并且毫无疑问,是按照正确的顺序还是错误的顺序执行某些操作。
山姆·艾克斯

谢谢,它对寻求替代答案很有用,对进行高效的批评也
很有帮助

7

使用相关的子查询。很简单,在这里您可以:

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

该代码可能并不完全正确,但是我确定这个想法是正确的。

GROUP BY是为了防止日期多次出现,您只希望在结果集中看到一次。

如果您不介意看到重复的日期,或者想查看原始值和ID,则需要以下内容:

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

谢谢...简单很棒。有一个要增加性能的索引,但这很简单(采用Database Engine Tuning Advisor的建议之一),然后运行起来就像一个镜头。
Doug_Ivison


4

假设窗口可以像在其他地方(我已经尝试过)一样在SQL Server 2008上运行,请尝试一下:

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

MSDN说它可以在SQL Server 2008中使用(也许也可以在2005中使用),但是我没有实例可以尝试。

编辑:好吧,显然,SQL Server不允许在不指定“ PARTITION BY”的情况下进行窗口指定(“ OVER(...)”)(将结果分成几组,但不能以GROUP BY的方式进行汇总)。烦人的-MSDN语法参考表明它是可选的,但目前我只有SqlServer 2000实例。

我提供的查询在Oracle 10.2.0.3.0和PostgreSQL 8.4-beta中均有效。所以告诉MS赶上;)


2
在这种情况下,将OVER与SUM一起使用将无法获得总计。与SUM一起使用时,OVER子句不接受ORDER BY。您必须使用PARTITION BY,它将无法用于运行总计。
山姆·

谢谢,它对于听到为什么不起作用实际上很有用。araqnid也许您可以编辑答案以解释为什么它不是一个选择
2009年


这实际上对我有用,因为我需要分区-因此,即使这不是最受欢迎的答案,它也是解决我的SQL RT问题的最简单解决方案。
William MB

我没有MSSQL 2008,但我认为您可能可以按(选择null)进行分区,并解决分区问题。或使用进行子选择1 partitionme并以此进行分区。此外,在现实生活中进行报告时可能需要按分区。
nurettin

4

如果您使用的是上面的Sql Server 2008 R2。然后,这将是最短的方法。

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

LAG用于获取上一行的值。您可以通过google获取更多信息。

[1]:


1
我相信LAG仅存在于SQL Server 2012及更高版本中(而不是2008)
AaA

1
使用LAG()并没有改善,SUM(somevalue) OVER(...) 这对我来说似乎更干净
Used_By_Already

2

我相信可以使用下面的简单INNER JOIN操作来实现运行总计。

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

是的,我认为这相当于Sam Saffron回答中的“测试3”。
codeulike 2011年

2

以下将产生所需的结果。

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

在SomeDate上具有聚集索引将大大提高性能。


@戴夫,我认为这个问题试图找到一种有效的方法,对于大套来说,交叉连接将非常缓慢
Sam Saffron

谢谢,它对寻求替代答案很有用,对进行高效的批评也
很有帮助


2

尽管最好的方法是使用window函数,但也可以使用简单的相关子查询来完成。

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

您可能应该提供一些有关您在这里做什么的信息,并注意此特定方法的任何优点/缺点。
TT。

0

这里有2种简单的方法来计算运行总计:

方法1:如果您的DBMS支持分析功能,则可以用这种方式编写

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

方法2:如果您的数据库版本/ DBMS本身不支持分析功能,则可以使用OUTER APPLY

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;

注意:-如果必须分别计算不同分区的运行总计,则可以按此处发布的操作进行:计算行之间的运行总计并按ID分组

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.