计算运行总计/运行余额

Posted

技术标签:

【中文标题】计算运行总计/运行余额【英文标题】:Calculate running total / running balance 【发布时间】:2012-07-03 21:11:21 【问题描述】:

我有一张桌子:

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

基本上第一个记录余额将与amt 相同,第二个以后的余额将是先前余额+当前amt 的加法。我正在寻找一种最佳方法。我可以考虑使用函数或相关子查询,但不知道该怎么做。

【问题讨论】:

如果您发布代码、XML 或数据示例,在文本编辑器中突出显示这些行,然后单击编辑器上的“代码示例”按钮 ( )工具栏以很好地格式化和语法突出显示它! 除了 TID 字段之外,还有什么代表交易的排序标准? TID 字段是唯一的排序标准吗?请记住,记录的排序顺序会影响您作为计算值寻求的运行余额。 How do I calculate a running total in SQL without using a cursor?的可能重复 您目前没有修复 RBS 的交易,是吗? :) 我不同意重复的建议——不是因为它不是同一个问题,而是因为那里接受的答案使用了古怪的更新方法(实际上只是指向一个描述古怪更新方法的链接),它是' 不受支持或记录,并且不保证(现在或将来)工作。 【参考方案1】:

如果你使用的是 2012 版本,这里有一个解决方案

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

【讨论】:

正如我在回答中发布的那样,请小心使用此方法。默认情况下,SUM() OVER() 使用 RANGE UNBOUNDED PRECEDING,它使用磁盘假脱机。随着源数据变大,您将真正看到这个磁盘假脱机的影响。如果您使用ROWS UNBOUNDED PRECEDING,它将在内存中发生,直到您达到非常高端... 这里如何使用 where 子句并获得结果的原始总数?【参考方案2】:

对于那些不使用 SQL Server 2012 或更高版本的用户,游标可能是 CLR 之外最有效的支持保证方法。还有其他方法,例如“古怪的更新”,它可能会稍微快一点,但不能保证在未来工作,当然还有基于集合的方法,随着表变大,具有双曲线性能配置文件,以及递归 CTE 方法,通常需要直接#tempdb I/O 或导致产生大致相同影响的溢出。


INNER JOIN - 不要这样做:

缓慢的、基于集合的方法是这样的:

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 行。这是指数级的,并且会出现故障、超时或只是愤怒的用户。


相关子查询 - 也不要这样做:

由于同样痛苦的原因,子查询表单同样痛苦。

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;

古怪的更新 - 风险自负:

“古怪的更新”方法比上述方法更有效,但没有记录该行为,无法保证顺序,并且该行为今天可能有效,但将来可能会中断。我包括这个是因为它是一种流行的方法并且它很有效,但这并不意味着我认可它。我什至回答这个问题而不是将其作为重复项关闭的主要原因是因为the other question has a quirky update as the accepted answer。

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;

递归 CTE

第一个依赖 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() 构建一个连续的序列:

;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 或更高版本)。


光标

每个人都被告知游标是邪恶的,应该不惜一切代价避免它们,但这实际上优于大多数其他支持的方法,并且比古怪的更新更安全。我更喜欢游标解决方案的唯一方法是 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 或更高版本,则选择很明显 - 使用新的 SUM() OVER() 构造(使用 ROWSRANGE)。对于早期版本,您需要比较替代方法在架构、数据上的性能,并考虑到与性能无关的因素,确定哪种方法适合您。这很可能是CLR方法。以下是我的建议,按优先顺序排列:

    SUM() OVER() ... ROWS,如果是 2012 年或更高版本 CLR 方法,如果可能的话 如果可能,第一个递归 CTE 方法 光标 其他递归 CTE 方法 古怪的更新 加入和/或关联子查询

有关这些方法的性能比较的更多信息,请参阅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


对于分组/分区运行总计,请参阅以下帖子:

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

Partitioning results in a running totals query

Multiple Running Totals with Group By

【讨论】:

谢谢!只是想提一下,在递归 cte x.rt + y.amt 中,如果您的 amt 是十进制,则必须强制转换为十进制,否则会抛出“锚点和递归部分之间的类型不匹配” @Jack0fshad0ws 谢谢,一定要记住,但答案是基于amt int的问题。 @AaronBertrand 我不完全理解古怪更新方法的问题,我想知道它是否能毫无顾虑地满足我的需求。我必须在用户有积分的地方进行抽奖,每个积分都是一个机会。我没有任何命令。 A用户10分,B用户25分,Z用户15分。因此,我的累积机会可能是 A 10 10、B 25 35、Z 15 50。如果订单更改为 B 25 25、Z 15 40、A 10 50,我可以毫无错误地抽出 1 到 50 之间的随机数。我使用安全吗奇怪的更新? @Horaciux 我不确定我是否完全理解您为什么需要古怪的更新。但我目前没有能力确定你是否安全……你需要测试一下 @AaronBertrand 谢谢。我提出一个完整的问题。 dba.stackexchange.com/questions/83322/…【参考方案3】:

在 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

【讨论】:

【参考方案4】:

我们使用的是 2008R2,我使用变量和临时表。这还允许您在使用 case 语句计算每一行时执行自定义操作(即某些交易可能会有所不同,或者您可能只需要特定交易类型的总计)

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 个事务,并且针对它运行这种类型的查询根本不需要时间。

【讨论】:

【参考方案5】:
select v.ID
,CONVERT(VARCHAR(10), v.EntryDate, 103) + ' '  + convert(VARCHAR(8), v.EntryDate, 14) 
as EntryDate
,case
when v.CreditAmount<0
then
    ISNULL(v.CreditAmount,0) 
    else 
    0 
End  as credit
,case
when v.CreditAmount>0
then
    v.CreditAmount
    else
    0
End  as debit
,Balance = SUM(v.CreditAmount) OVER (ORDER BY v.ID ROWS UNBOUNDED PRECEDING)
      from VendorCredit v
    order by v.EntryDate desc

【讨论】:

【参考方案6】:

使用 2012 年的 SUMOVER 函数,您现在可以嵌套 sumcounts

SELECT date, sum(count(DISTINCT unique_id)) OVER (ORDER BY date) AS total_per_date
FROM dbo.table
GROUP BY date

【讨论】:

以上是关于计算运行总计/运行余额的主要内容,如果未能解决你的问题,请参考以下文章

s-s-rS-如何检索以前的余额以便计算当前余额(总计 + 前一天的余额)使用 tablix 报告现金流

SQL窗口函数和运行总计

如何根据上个月的计算更新每个月的期初余额?

使用 BigQuery 中的条件计算运行总计

SQL Server 2012 Windowing 函数计算运行总计

在 SQL Server 中计算运行总计