我正在寻找一种快速的方法来创建一个大型SQL Server 2008数据集中的累积总计,该数据集按特定列进行分区,可能使用多分配变量解决方案。作为一个非常基本的示例,我想在下面创建“cumulative_total”列:
user_id | month | total | cumulative_total
1 | 1 | 2.0 | 2.0
1 | 2 | 1.0 | 3.0
1 | 3 | 3.5 | 8.5
2 | 1 | 0.5 | 0.5
2 | 2 | 1.5 | 2.0
2 | 3 | 2.0 | 4.0
我们传统上使用相关子查询来完成此操作,但是对于大量数据(200,000多行和几个不同类别的运行总计),这并没有给我们提供理想的性能。
我最近在这里阅读了有关使用多个赋值变量进行累积求和的信息:
http://sqlblog.com/blogs/paul_nielsen/archive/2007/12/06/cumulative-totals-screencast.aspx
在该博客的示例中,累积变量解决方案如下所示:
UPDATE my_table
SET @CumulativeTotal=cumulative_total=@CumulativeTotal+ISNULL(total, 0)
在上面的示例(用户1或用户2)中,对于单个用户的求和,此解决方案似乎非常快。但是,我需要按用户进行有效分区 - 按月按用户累计累计。
是否有人知道扩展多重赋值变量概念以解决此问题的方法,或除相关子查询或游标之外的任何其他想法?
非常感谢任何提示。
答案 0 :(得分:6)
如果您不需要存储数据(您不应该存储数据,因为您需要在任何行更改,添加或删除任何行时更新运行总计),如果您不信任奇怪的更新(您不应该这样做,因为它不能保证工作,并且它的行为可能会随着修补程序,Service Pack,升级甚至基础索引或统计信息更改而改变),您可以在运行时尝试这种类型的查询。这是MVP Hugo Kornelis创造的“基于集合的迭代”的方法(他在SQL Server MVP Deep Dives的一个章节中发布了类似的内容)。由于运行总计通常需要在整个集合上使用光标,对整个集合进行奇怪的更新,或者随着行计数增加而变得越来越昂贵的单个非线性自联接,这里的技巧是循环一些有限的集合中的元素(在这种情况下,每个用户的每行的“等级”,对于每个用户 - 并且您只处理该等级的所有用户/月份组合的每个等级一次,因此不是循环遍历200,000行,你最多循环24次)。
DECLARE @t TABLE
(
[user_id] INT,
[month] TINYINT,
total DECIMAL(10,1),
RunningTotal DECIMAL(10,1),
Rnk INT
);
INSERT @t SELECT [user_id], [month], total, total,
RANK() OVER (PARTITION BY [user_id] ORDER BY [month])
FROM dbo.my_table;
DECLARE @rnk INT = 1, @rc INT = 1;
WHILE @rc > 0
BEGIN
SET @rnk += 1;
UPDATE c SET RunningTotal = p.RunningTotal + c.total
FROM @t AS c INNER JOIN @t AS p
ON c.[user_id] = p.[user_id]
AND p.rnk = @rnk - 1
AND c.rnk = @rnk;
SET @rc = @@ROWCOUNT;
END
SELECT [user_id], [month], total, RunningTotal
FROM @t
ORDER BY [user_id], rnk;
结果:
user_id month total RunningTotal
------- ----- ----- ------------
1 1 2.0 2.0
1 2 1.0 3.0
1 3 3.5 6.5 -- I think your calculation is off
2 1 0.5 0.5
2 2 1.5 2.0
2 3 2.0 4.0
当然你可以从这个表变量中更新基表,但是为什么要这么麻烦,因为这些存储的值只有在下次任何DML语句触及表时才会有效?
UPDATE mt
SET cumulative_total = t.RunningTotal
FROM dbo.my_table AS mt
INNER JOIN @t AS t
ON mt.[user_id] = t.[user_id]
AND mt.[month] = t.[month];
由于我们不依赖于任何类型的隐式排序,因此这是100%支持的,并且相对于不受支持的古怪更新,应该进行性能比较。即使它没有击败但接近它,你应该考虑使用它无论如何恕我直言。
对于SQL Server 2012解决方案,Matt提到RANGE
,但由于此方法使用磁盘假脱机,因此您还应使用ROWS
进行测试,而不是仅使用RANGE
进行测试。以下是您案例的快速示例:
SELECT
[user_id],
[month],
total,
RunningTotal = SUM(total) OVER
(
PARTITION BY [user_id]
ORDER BY [month] ROWS UNBOUNDED PRECEDING
)
FROM dbo.my_table
ORDER BY [user_id], [month];
将此与RANGE UNBOUNDED PRECEDING
或根本没有ROWS\RANGE
进行比较(这也将使用RANGE
磁盘假脱机)。即使计划看起来稍微复杂一些(一个额外的序列项目运营商),上面的总持续时间也会更短,而且方式更少的I / O.
我最近发布了一篇博客文章,概述了我在特定运行总计方案中观察到的一些性能差异:
http://www.sqlperformance.com/2012/07/t-sql-queries/running-totals
答案 1 :(得分:2)
您在SQL Server 2008中的选项相当有限 - 因为您可以根据上述方法执行某些操作(称为“quirky update”),或者您可以在CLR中执行某些操作。
就个人而言,我会选择CLR,因为它可以保证工作,而古怪的更新语法不是正式支持的(因此在将来的版本中可能会中断)。
您正在寻找的古怪更新语法的变体类似于:
UPDATE my_table
SET @CumulativeTotal=cumulative_total=ISNULL(total, 0) +
CASE WHEN @user=@lastUser THEN @CumulativeTotal ELSE 0 END,
@user=lastUser
值得注意的是,SQL Server 2012在窗口函数中引入了RANGE
支持,因此可以通过最有效的方式表达,同时100%支持。