2

我正在寻找一种快速方法来在按特定列分区的大型 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)求和,此解决方案似乎非常快。但是,我需要按用户有效分区 - 给我按月按用户的累计总数。

有谁知道扩展多个赋值变量概念来解决这个问题的方法,或者除了相关子查询或游标之外的任何其他想法?

非常感谢任何提示。

4

2 回答 2

6

如果您不需要存储数据(您不应该这样做,因为您需要在任何行更改、添加或删除时更新运行总计),并且如果您不信任古怪的更新(您不应该,因为它不能保证工作,并且它的行为可能会随着修补程序、服务包、升级甚至基础索引或统计信息的变化而改变),您可以在运行时尝试这种类型的查询。这是 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

于 2012-05-31T12:23:22.570 回答
2

您在 SQL Server 2008 中的选择是相当有限的 - 因为您可以根据上述方法(称为“古怪更新”)执行某些操作,或者您可以在 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% 得到支持。

于 2012-05-31T11:01:14.420 回答