在 SQL Server 中计算运行总计
Posted
技术标签:
【中文标题】在 SQL Server 中计算运行总计【英文标题】:Calculate a Running Total in SQL Server 【发布时间】:2009-05-13 23:42:26 【问题描述】:想象一下下表(称为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
我想要一个按日期顺序返回运行总计的查询,例如:
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 中有 various ways of doing this。
我对这种使用聚合集语句技巧的方法特别感兴趣:
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
语句会以正确的顺序处理行。也许我们可以得到一些关于这个问题的明确答案。
但也许人们可以提出其他建议?
编辑:现在使用SqlFiddle 设置和上面的“更新技巧”示例
【问题讨论】:
blogs.msdn.com/sqltips/archive/2005/07/20/441053.aspx 将订单添加到您的更新...集,您将获得保证。 但是 Order by 不能应用于 UPDATE 语句...可以吗? 另请参阅sqlperformance.com/2012/07/t-sql-queries/running-totals,尤其是如果您使用的是 SQL Server 2012。 【参考方案1】:更新,如果您运行的是 SQL Server 2012,请参阅:https://***.com/a/10309947
问题在于 Over 子句的 SQL Server 实现是somewhat limited。
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 没有为您提供此问题的干净解决方案。我的直觉告诉我,这是光标最快的罕见情况之一,尽管我必须对大结果进行一些基准测试。
更新技巧很方便,但我觉得它相当脆弱。似乎如果您正在更新一个完整的表,那么它将按照主键的顺序进行。因此,如果您将日期设置为主键升序,您将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 技巧。我唯一担心的是,我不确定在所有情况下更新都会以线性方式进行。查询中没有任何内容明确说明。
底线,对于生产代码,我会使用光标。
测试数据:
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
测试 1:
SELECT ord,total,
(SELECT SUM(total)
FROM #t b
WHERE b.ord <= a.ord) AS b
FROM #t a
-- CPU 11731, Reads 154934, Duration 11135
测试 2:
SELECT a.ord, a.total, SUM(b.total) AS RunningTotal
FROM #t a CROSS JOIN #t b
WHERE (b.ord <= a.ord)
GROUP BY a.ord,a.total
ORDER BY a.ord
-- CPU 16053, Reads 154935, Duration 4647
测试 3:
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
测试 4:
declare @total int
set @total = 0
update #t set running_total = @total, @total = @total + total
select * from #t
-- CPU 0, Reads 58, Duration 139
【讨论】:
谢谢。所以你的代码示例是为了证明它将按主键的顺序求和,我想。想知道游标是否仍然比连接更大的数据集更有效。 我刚刚测试了 CTE @Martin,没有什么能比得上更新技巧 - 读取时光标似乎较低。这是探查器跟踪i.stack.imgur.com/BbZq3.png @Martin Denali 将为msdn.microsoft.com/en-us/library/ms189461(v=SQL.110).aspx提供一个非常好的解决方案@ +1 为这个答案所做的所有工作 - 我喜欢 UPDATE 选项;可以在此 UPDATE 脚本中构建分区吗?例如,如果有一个附加字段“汽车颜色”,此脚本能否返回每个“汽车颜色”分区内的运行总数? 初始(Oracle(和 ANSI-SQL))答案现在可以在 SQL Server 2017 中使用。谢谢,非常优雅!【参考方案2】:在 SQL Server 2012 中,您可以将 SUM() 与 OVER() 子句一起使用。
select id,
somedate,
somevalue,
sum(somevalue) over(order by somedate rows unbounded preceding) as runningtotal
from TestTable
SQL Fiddle
【讨论】:
【参考方案3】:虽然 Sam Saffron 在这方面做得很好,但他仍然没有为这个问题提供递归公用表表达式代码。对于使用 SQL Server 2008 R2 而不是 Denali 的我们来说,它仍然是获得运行总数的最快方法,它比我的工作计算机上 100000 行的光标快大约 10 倍,而且它也是内联查询。
所以,这里是(我假设表中有一个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 fiddle demo
更新
我也很好奇这个变量更新或古怪更新。所以通常它工作正常,但我们如何确定它每次都能工作?好吧,这里有一个小技巧(在这里找到 - 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
的索引)更新将一直以线性方式进行(从未遇到除以零)。也就是说,由您决定是否要在生产代码中使用它:)
更新 2 我正在链接这个答案,因为它包含一些关于古怪更新不可靠性的有用信息 - nvarchar concatenation / index / nvarchar(max) inexplicable behavior。
【讨论】:
这个答案值得更多的认可(或者它可能有一些我看不到的缺陷?) 应该有一个序号,这样你就可以加入 ord = ord + 1 ,有时它需要更多的工作。但无论如何,在 SQL 2008 R2 上,我正在使用这个解决方案 如果您已经拥有数据的序数并且您正在寻找基于 SQL 2008 R2 的简洁(非游标)集的解决方案,这似乎是完美的。 不是每个运行的总查询都会有一个连续的序号字段。有时日期时间字段就是您所拥有的,或者记录已从排序中间删除。这可能是它没有被更频繁地使用的原因。 @Reuben 如果你的表足够小,你总是可以将它转储到带有序号的临时表中,但是是的,有时这个解决方案不容易应用【参考方案4】: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
【讨论】:
适用于较小的数据集。缺点是您必须在内部和外部查询中使用相同的 where 子句。 由于我的一些日期完全相同(精确到几分之一秒),我不得不添加:row_number() over (order by txndate) 到内部和外部表和一些复合使其运行的索引。光滑/简单的解决方案。顺便说一句,针对子查询进行了测试交叉应用......它稍微快一点。 这非常干净,并且在小型数据集上运行良好;比递归 CTE 更快 这也是一个不错的解决方案(对于小型数据集),但您还必须注意它意味着 somedate 列是唯一的【参考方案5】: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() 函数和临时表创建任意列,以用于内部 SELECT 语句的比较。
【讨论】:
这确实效率低下......但是在 sql server 中也没有真正干净的方法 这绝对是低效的——但它确实能胜任,而且毫无疑问是否按正确或错误的顺序执行某事。 谢谢,有其他答案很有用,对有效的批评也很有用【参考方案6】:使用相关子查询。很简单,给你:
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 是在一个日期出现多次的情况下,您只想在结果集中看到一次。
如果您不介意看到重复的日期,或者您想查看原始值和 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
【讨论】:
谢谢...简单很棒。有一个要添加的索引来提高性能,但这很简单,(从 Database Engine Tuning Advisor 中获得一个建议;),然后它就像一个镜头一样运行。【参考方案7】:您还可以非规范化 - 将运行总计存储在同一个表中:
http://sqlblog.com/blogs/alexander_kuznetsov/archive/2009/01/23/denormalizing-to-enforce-business-rules-running-totals.aspx
Selects 的工作速度比任何其他解决方案都要快,但修改可能会更慢
【讨论】:
【参考方案8】:如果您使用的是 Sql server 2008 R2 以上版本。那么,这将是最短的方法;
Select id
,somedate
,somevalue,
LAG(runningtotal) OVER (ORDER BY somedate) + somevalue AS runningtotal
From TestTable
LAG 用于获取上一行的值。你可以谷歌了解更多信息。
[1]:
【讨论】:
我相信LAG只存在于SQL server 2012及以上(不是2008) 使用 LAG() 并没有改善SUM(somevalue) OVER(...)
,这对我来说似乎更干净【参考方案9】:
假设窗口在 SQL Server 2008 上的工作方式与其他地方一样(我已经尝试过),试一试:
select testtable.*, sum(somevalue) over(order by somedate)
from testtable
order by somedate;
MSDN 说它在 SQL Server 2008(也许 2005 也可以?)中可用,但我没有实例可供试用。
编辑:好吧,显然 SQL Server 不允许在没有指定“PARTITION BY”的情况下使用窗口规范(“OVER(...)”)(将结果分成组,但不像 GROUP BY 那样聚合)。烦人——MSDN 语法参考表明它是可选的,但我目前只有 SqlServer 2000 实例。
我给出的查询在 Oracle 10.2.0.3.0 和 PostgreSQL 8.4-beta 中都有效。所以告诉 MS 赶上 ;)
【讨论】:
在这种情况下,将 OVER 与 SUM 一起使用将无法提供运行总计。 OVER 子句在与 SUM 一起使用时不接受 ORDER BY。您必须使用 PARTITION BY,它不适用于运行总计。 谢谢,听听为什么这不起作用真的很有用。 araqnid 也许你可以编辑你的答案来解释为什么它不是一个选项 Coming in SQL Server 2011 apparently 这实际上对我有用,因为我需要分区 - 所以即使这不是最流行的答案,它也是我在 SQL 中的 RT 问题的最简单解决方案。 我没有 MSSQL 2008,但我认为您可以通过 (select null) 分区并解决分区问题。或者使用1 partitionme
进行子选择并以此进行分区。此外,在现实生活中进行报告时可能需要分区。【参考方案10】:
虽然完成它的最佳方法是使用窗口函数,但也可以使用简单的相关子查询来完成。
Select id, someday, somevalue, (select sum(somevalue)
from testtable as t2
where t2.id = t1.id
and t2.someday <= t1.someday) as runningtotal
from testtable as t1
order by id,someday;
【讨论】:
【参考方案11】:我相信可以使用下面的简单 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
【讨论】:
是的,我认为这相当于 Sam Saffron 回答中的“测试 3”。【参考方案12】:以下将产生所需的结果。
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 上有一个聚集索引将大大提高性能。
【讨论】:
@Dave 我认为这个问题试图找到一种有效的方法,对于大型集合,交叉连接会非常慢 谢谢,有其他答案很有用,对有效的批评也很有用【参考方案13】:使用连接 另一种变体是使用连接。现在查询可能如下所示:
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
【讨论】:
【参考方案14】:这里有 2 种简单的方法来计算总和:
方法 1:如果您的 DBMS 支持分析函数,则可以这样编写
SELECT id
,somedate
,somevalue
,runningtotal = SUM(somevalue) OVER (ORDER BY somedate ASC)
FROM TestTable
方法 2:如果您的数据库版本/DBMS 本身不支持分析功能,您可以使用 OUTER APPLY
SELECT T.id
,T.somedate
,T.somevalue
,runningtotal = OA.runningtotal
FROM TestTable T
OUTER APPLY (
SELECT runningtotal = SUM(TI.somevalue)
FROM TestTable TI
WHERE TI.somedate <= S.somedate
) OA;
注意:- 如果您必须分别计算不同分区的运行总数,可以按照此处发布的方式完成:Calculating Running totals across rows and grouping by ID
【讨论】:
【参考方案15】: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
【讨论】:
你可能应该提供一些关于你在这里做什么的信息,并注意这种特定方法的任何优点/缺点。以上是关于在 SQL Server 中计算运行总计的主要内容,如果未能解决你的问题,请参考以下文章
SQL Server 2012 Windowing 函数计算运行总计