计算运行总额/运行余额

我有一张桌子:

create table Transactions(Tid int,amt int) 

5行:

 insert into Transactions values(1, 100) insert into Transactions values(2, -50) insert into Transactions values(3, 100) insert into Transactions values(4, -100) insert into Transactions values(5, 200) 

期望的输出:

 TID amt balance --- ----- ------- 1 100 100 2 -50 50 3 100 150 4 -100 50 5 200 250 

基本上第一笔logging的余额将会与第二笔余额相同,第二笔余额将会加上以前的余额+当前的amt 。 我正在寻找一个最佳的方法。 我可以考虑使用函数或相关的子查询,但不知道如何做到这一点。

对于那些不使用SQL Server 2012或更高版本的用户,游标可能是CLR之外最有效的支持保证的方法。 还有其他一些方法,比如“古怪的更新”,它可以稍微快一些,但不能保证在将来能够正常工作;当然,当表格变大时,基于集合的双曲线性能曲线的方法,以及通常需要直接进行recursion的CTE方法#tempdb I / O或导致产生大致相同影响的溢出。


内部join – 不要这样做:

缓慢的,基于集合的方法的forms是:

 SELECT t1.TID, t1.amt, RunningTotal = SUM(t2.amt) FROM dbo.Transactions AS t1 INNER JOIN dbo.Transactions AS t2 ON t1.TID >= t2.TID GROUP BY t1.TID, t1.amt ORDER BY t1.TID; 

这是慢的原因? 随着表格变大,每个增量行需要读取表中的n-1行。 这是指数性的,并且对于失败,超时或者只是生气的用户而言是有限的。


相关的子查询 – 不要这样做:

子查询forms同样痛苦,同样痛苦的原因。

 SELECT TID, amt, RunningTotal = amt + COALESCE( ( SELECT SUM(amt) FROM dbo.Transactions AS i WHERE i.TID < o.TID), 0 ) FROM dbo.Transactions AS o ORDER BY TID; 

古怪的更新 – 请自行承担风险:

“古怪的更新”方法比上述方法更有效率,但行为没有logging,没有关于秩序的保证,行为今天可能工作,但可能在未来打破。 我包括这个,因为它是一个stream行的方法,它是有效的,但这并不意味着我支持它。 我甚至回答这个问题的主要原因,而不是把它作为一个副本closures,因为另一个问题有一个古怪的更新作为接受的答案 。

 DECLARE @t TABLE ( TID INT PRIMARY KEY, amt INT, RunningTotal INT ); DECLARE @RunningTotal INT = 0; INSERT @t(TID, amt, RunningTotal) SELECT TID, amt, RunningTotal = 0 FROM dbo.Transactions ORDER BY TID; UPDATE @t SET @RunningTotal = RunningTotal = @RunningTotal + amt FROM @t; SELECT TID, amt, RunningTotal FROM @t ORDER BY TID; 

recursionCTE

这第一个依靠TID是连续的,没有差距:

 ;WITH x AS ( SELECT TID, amt, RunningTotal = amt FROM dbo.Transactions WHERE TID = 1 UNION ALL SELECT y.TID, y.amt, x.RunningTotal + y.amt FROM x INNER JOIN dbo.Transactions AS y ON y.TID = x.TID + 1 ) SELECT TID, amt, RunningTotal FROM x ORDER BY TID OPTION (MAXRECURSION 10000); 

如果你不能依赖这个,那么你可以使用这个变体,它只是使用ROW_NUMBER()build立一个连续的序列:

 ;WITH y AS ( SELECT TID, amt, rn = ROW_NUMBER() OVER (ORDER BY TID) FROM dbo.Transactions ), x AS ( SELECT TID, rn, amt, rt = amt FROM y WHERE rn = 1 UNION ALL SELECT y.TID, y.rn, y.amt, x.rt + y.amt FROM x INNER JOIN y ON y.rn = x.rn + 1 ) SELECT TID, amt, RunningTotal = rt FROM x ORDER BY x.rn OPTION (MAXRECURSION 10000); 

根据数据的大小(例如我们不知道的列),通过首先将相关列仅填充到#temp表中,然后对其进行处理(而不是基表),您可能会发现更好的整体性能:

 CREATE TABLE #x ( rn INT PRIMARY KEY, TID INT, amt INT ); INSERT INTO #x (rn, TID, amt) SELECT ROW_NUMBER() OVER (ORDER BY TID), TID, amt FROM dbo.Transactions; ;WITH x AS ( SELECT TID, rn, amt, rt = amt FROM #x WHERE rn = 1 UNION ALL SELECT y.TID, y.rn, y.amt, x.rt + y.amt FROM x INNER JOIN #x AS y ON y.rn = x.rn + 1 ) SELECT TID, amt, RunningTotal = rt FROM x ORDER BY TID OPTION (MAXRECURSION 10000); DROP TABLE #x; 

只有第一个CTE方法才能提供与古怪的更新相媲美的性能,但是对数据的性质(没有差距)作了大的假设。 其他两种方法会回退,在这种情况下,您可以使用游标(如果您不能使用CLR,而您还不在SQL Server 2012或更高版本中)。


光标

每个人都被告知,游标是邪恶的,应该不惜一切代价避免,但是这实际上比其他大多数支持方法的性能要好,而且比这个古怪的更新更安全。 我唯一比光标解决scheme更喜欢的是2012和CLR方法(如下):

 CREATE TABLE #x ( TID INT PRIMARY KEY, amt INT, rt INT ); INSERT #x(TID, amt) SELECT TID, amt FROM dbo.Transactions ORDER BY TID; DECLARE @rt INT, @tid INT, @amt INT; SET @rt = 0; DECLARE c CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY FOR SELECT TID, amt FROM #x ORDER BY TID; OPEN c; FETCH c INTO @tid, @amt; WHILE @@FETCH_STATUS = 0 BEGIN SET @rt = @rt + @amt; UPDATE #x SET rt = @rt WHERE TID = @tid; FETCH c INTO @tid, @amt; END CLOSE c; DEALLOCATE c; SELECT TID, amt, RunningTotal = rt FROM #x ORDER BY TID; DROP TABLE #x; 

SQL Server 2012或更高版本

在SQL Server 2012中引入的新窗口函数使得这个任务变得更容易了(它也比上面的所有方法都要好):

 SELECT TID, amt, RunningTotal = SUM(amt) OVER (ORDER BY TID ROWS UNBOUNDED PRECEDING) FROM dbo.Transactions ORDER BY TID; 

请注意,在较大的数据集上,您会发现上述性能比以下两个选项中的任何一个都要好得多,因为RANGE使用了磁盘上的假脱机(并且默认使用了RANGE)。 然而,重要的是要注意,行为和结果可能会有所不同,所以要确定它们都返回正确的结果,然后根据这种差异来决定它们之间的结果。

 SELECT TID, amt, RunningTotal = SUM(amt) OVER (ORDER BY TID) FROM dbo.Transactions ORDER BY TID; SELECT TID, amt, RunningTotal = SUM(amt) OVER (ORDER BY TID RANGE UNBOUNDED PRECEDING) FROM dbo.Transactions ORDER BY TID; 

CLR

为了完整起见,我提供了一个Pavel Pawlowski的CLR方法的链接,这是迄今为止SQL Server 2012之前的版本(但显然不是2000)的更好方法。

http://www.pawlowski.cz/2010/09/sql-server-and-fastest-running-totals-using-clr/


结论

如果您使用的是SQL Server 2012或更高版本,则select很明显 – 使用新的SUM() OVER()构造(使用ROWS vs. RANGE )。 对于早期版本,您需要比较架构,数据上的其他方法的性能,并考虑与性能无关的因素 – 确定哪种方法适合您。 这很可能是CLR方法。 这是我的build议,按照优先顺序:

  1. SUM() OVER() ... ROWS ,如果在2012或以上
  2. CLR方法,如果可能的话
  3. 如果可能的话,先recursionCTE方法
  4. 光标
  5. 其他的recursionCTE方法
  6. 古怪的更新
  7. join和/或关联的子查询

有关这些方法性能比较的更多信息,请参阅http://dba.stackexchange.com上的此问题:;

https://dba.stackexchange.com/questions/19507/running-total-with-count


我也在这里博客了更多关于这些比较的细节:

http://www.sqlperformance.com/2012/07/t-sql-queries/running-totals


另外,对于分组/分区运行总数,请参阅以下post:

http://sqlperformance.com/2014/01/t-sql-queries/grouped-running-totals

分区会导致运行总计查询

分组依据多个运行总计

如果您使用版本2012,这是一个解决scheme

 select *, sum(amt) over (order by Tid) as running_total from Transactions 

对于早期版本

 select *,(select sum(amt) from Transactions where Tid<=t.Tid) as running_total from Transactions as t 

我们在2008R2,我使用variables和临时表。 这还允许您在使用case语句计算每行时执行自定义操作(例如,某些事务的行为可能会有所不同,或者您可能只需要特定事务types的总数)

 DECLARE @RunningBalance int = 0 SELECT Tid, Amt, 0 AS RunningBalance INTO #TxnTable FROM Transactions ORDER BY Tid UPDATE #TxnTable SET @RunningBalance = RunningBalance = @RunningBalance + Amt SELECT * FROM #TxnTable DROP TABLE #TxnTable 

我们有一个拥有230万行的交易表,其中一个项目有3,300多笔交易,而运行这种types的查询则根本不需要任何时间。

在SQL Server 2008+中

 SELECT T1.* , T2.RunningSum FROM dbo.Transactions As T1 CROSS APPLY ( SELECT SUM(amt) AS RunningSum FROM dbo.Transactions AS CAT1 WHERE ( CAT1.TId <= T1.TId ) ) AS T2 

在SQL Server 2012+中

 SELECT * , SUM(T1.amt) OVER ( ORDER BY T1.TId ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS RunningTotal FROM dbo.Transactions AS t1