计算在SQL Server中的运行总计

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

我想要一个查询,按date顺序返回一个正在运行的总数,如:

 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中有这样做的各种方法 。

我对这种使用聚合语句技巧的方法特别感兴趣:

 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语句将按正确的顺序处理行。 也许我们可以得到关于这个问题的一些明确的答案。

但也许还有别的方法可以让人build议呢?

编辑:现在用一个SqlFiddle与上面的设置和'更新技巧'的例子

更新 ,如果您正在运行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给你没有干净的解决scheme,这个问题。 我的直觉告诉我,这是光标最快的罕见情况之一,但是我将不得不做一些基准testing。

更新技巧是方便的,但我觉得它相当脆弱。 看来,如果你正在更新一个完整的表,那么它将按照主键的顺序进行。 所以如果你把你的date设置为主键升序,你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 

你要求一个基准,这是内部的。

最快的安全方式就是光标,它比交叉连接的相关子查询要快一个数量级。

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

底线,对于生产代码我会用光标去。

testing数据:

 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 

testing1:

 SELECT ord,total, (SELECT SUM(total) FROM #tb WHERE b.ord <= a.ord) AS b FROM #ta -- CPU 11731, Reads 154934, Duration 11135 

testing2:

 SELECT a.ord, a.total, SUM(b.total) AS RunningTotal FROM #ta CROSS JOIN #tb WHERE (b.ord <= a.ord) GROUP BY a.ord,a.total ORDER BY a.ord -- CPU 16053, Reads 154935, Duration 4647 

testing3:

 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 

testing4:

 declare @total int set @total = 0 update #t set running_total = @total, @total = @total + total select * from #t -- CPU 0, Reads 58, Duration 139 

在SQL Server 2012中,您可以使用SUM()和OVER()子句。

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

SQL小提琴

虽然Sam Saffron在这方面做了很多工作,但他仍然没有为这个问题提供recursion公用表expression式代码。 对于使用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小提琴演示

更新我也很好奇这个更新与variables古怪的更新 。 所以通常它可以正常工作,但我们如何确保它每次都能正常工作? 好吧,这里有一个小技巧(在这里find – 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 

从我所看到的,如果你的表上有正确的聚集索引/主键(在我们的例子中它将是由ord_id索引)更新将ord_id以线性方式进行(从未遇到被零除)。 这就是说,这取决于你决定是否要在生产代码中使用它:)

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 
 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()函数和一个临时表来创build一个任意的列,以便在内部SELECT语句的比较中使用。

您也可以非规范化 – 将运行总计存储在同一个表中:

http://sqlblog.com/blogs/alexander_kuznetsov/archive/2009/01/23/denormalizing-to-enforce-business-rules-running-totals.aspx

select工作比任何其他解决scheme快得多,但修改可能会更慢

使用相关的子查询。 很简单,在这里你去:

 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是万一出现的date不止一次,您只想在结果集中看到一次。

如果你不介意看重复的date,或者你想看到原来的价值和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 

假设窗口在SQL Server 2008上的工作方式与其他地方(我试过的)一样,可以这样做:

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

MSDN说,它在SQL Server 2008(也许是2005年)中可用,但我没有一个实例来试试它。

编辑:好吧,很明显,SQL Server不允许窗口规范(“OVER(…)”)没有指定“PARTITION BY”(将结果分成组但不聚合在GROUP BY的方式)。 讨厌 – MSDN语法参考build议它可选,但我目前只有SqlServer 2000实例。

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

以下将产生所需的结果。

 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上有一个聚集索引将大大提高性能。

我相信使用下面简单的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 

使用join另一个变体是使用join。 现在查询可能看起来像:

  SELECT a.id, a.value, SUM(b.Value)FROM RunTotalTestData a, RunTotalTestData b WHERE b.id <= a.id GROUP BY a.id, a.value ORDER BY a.id; 

为更多您能访问这个链接http://askme.indianyouth.info/details/calculating-simple-running-totals-in-sql-server-12

如果您使用上面的Sql server 2008 R2。 那么,这将是最短的做法;

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

LAG用于获取前一行的值。 你可以做谷歌的更多信息。

[1]:

 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