计算运行总计/运行余额

Pri*_*esh 54 t-sql sql-server running-total

我有一张桌子:

create table Transactions(Tid int,amt int)
Run Code Online (Sandbox Code Playgroud)

有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)
Run Code Online (Sandbox Code Playgroud)

期望的输出:

TID  amt  balance
--- ----- -------
1    100   100
2    -50    50
3    100   150
4   -100    50
5    200   250
Run Code Online (Sandbox Code Playgroud)

基本上对于第一记录余额将是相同的amt,第二个向前余额将是先前余额+当前的余额amt.我正在寻找一种最佳方法.我可以考虑使用函数或相关子查询,但不确定如何做到这一点.

Aar*_*and 154

对于那些不使用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;
Run Code Online (Sandbox Code Playgroud)

这个原因很慢?随着表变大,每个增量行需要在表中读取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;
Run Code Online (Sandbox Code Playgroud)

奇怪的更新 - 这样做需要您自担风险:

"古怪的更新"方法比上述方法更有效,但行为没有记录,没有关于订单的保证,行为今天可能有用,但将来可能会中断.我包括这个因为它是一种流行的方法而且效率很高,但这并不意味着我赞同它.我甚至回答这个问题而不是将其作为副本关闭的主要原因是因为另一个问题有一个奇怪的更新作为接受的答案.

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;
Run Code Online (Sandbox Code Playgroud)

递归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);
Run Code Online (Sandbox Code Playgroud)

如果您不能依赖于此,那么您可以使用此变体,它只使用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);
Run Code Online (Sandbox Code Playgroud)

根据数据的大小(例如我们不知道的列),您可以通过首先在#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;
Run Code Online (Sandbox Code Playgroud)

只有第一种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;
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)

请注意,在较大的数据集上,您会发现上述选项比以下两个选项中的任何一个都要好得多,因为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;
Run Code Online (Sandbox Code Playgroud)

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()构造(使用ROWSvs. RANGE).对于早期版本,您需要比较架构,数据和替代方法的性能,并考虑非性能相关因素 - 确定哪种方法适合您.很可能是CLR方法.以下是我的建议,按优先顺序排列:

  1. SUM() OVER() ... ROWS,如果在2012年或以上
  2. CLR方法,如果可能的话
  3. 第一种递归CTE方法,如果可能的话
  4. 光标
  5. 其他递归CTE方法
  6. 奇怪的更新
  7. 加入和/或相关子查询

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

分区会导致运行总计查询

具有分组依据的多个运行总计


Mad*_*nan 6

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

select *, sum(amt) over (order by Tid) as running_total from Transactions 
Run Code Online (Sandbox Code Playgroud)

对于早期版本

select *,(select sum(amt) from Transactions where Tid<=t.Tid) as running_total from Transactions as t
Run Code Online (Sandbox Code Playgroud)

  • 正如我在答案中发布的那样,请谨慎使用此方法。默认情况下,“ SUM()OVER()”使用“ RANGE UNBOUNDED PRECEDING”,它使用磁盘上的假脱机。随着源数据变大,您将真正看到此磁盘假脱机的影响。如果您使用“ ROWS UNBOUNDED PRECEDING”,它将在内存中发生,直到达到最高端为止。 (2认同)