分区会导致运行总计查询

时间:2012-05-31 10:45:34

标签: sql-server sql-server-2008

我正在寻找一种快速的方法来创建一个大型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)中,对于单个用户的求和,此解决方案似乎非常快。但是,我需要按用户进行有效分区 - 按月按用户累计累计。

是否有人知道扩展多重赋值变量概念以解决此问题的方法,或除相关子查询或游标之外的任何其他想法?

非常感谢任何提示。

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%支持。