我有一张桌子:
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
的增加。我正在寻找一种最佳方法。我可以考虑使用函数或相关子查询,但不确定如何做到这一点。
答案 0 :(得分:148)
对于那些不使用SQL Server 2012或更高版本的用户,游标可能是CLR之外最有效的支持的和保证方法。还有其他一些方法,例如“古怪的更新”,可以稍微快一点,但不能保证在将来工作,当然还有基于集合的方法,随着表变大,双曲线性能曲线,以及通常需要直接的递归CTE方法#tempdb I / O或导致溢出产生大致相同的影响。
缓慢的,基于集合的方法具有以下形式:
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;
第一个依赖于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中引入的新窗口函数使这项任务变得更加容易(并且它的性能也优于上述所有方法):
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;
为了完整起见,我提供了一个链接到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()
构造(ROWS
与RANGE
)。对于早期版本,您需要比较架构,数据和替代方法的性能,并考虑非性能相关因素 - 确定哪种方法适合您。很可能是CLR方法。以下是我的建议,按优先顺序排列:
SUM() OVER() ... ROWS
,如果在2012年或以上有关这些方法的性能比较的更多信息,请参阅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
答案 1 :(得分:5)
如果您使用的是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
答案 2 :(得分:1)
我们在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个事务的项目,并且针对该事务运行此类查询根本不需要时间。
答案 3 :(得分:0)
在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 :(得分:0)
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
答案 5 :(得分:0)
借助2012 SUM
和OVER
函数,您现在可以嵌套sum
和counts
。
SELECT date, sum(count(DISTINCT unique_id)) OVER (ORDER BY date) AS total_per_date
FROM dbo.table
GROUP BY date