2012-05-31 123 views
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 @[email protected]+ISNULL(total, 0) 

该解决方案似乎在上面的例子(用户1或用户2)中对于单个用户的总结速度非常快。但是,我需要按用户进行有效分区 - 按月向用户提供累计总额。

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

非常感谢您的任何提示。

+0

你有大量的用户或大量个月或两者?也是什么版本的SQL Server? –

+0

嗨亚伦。大量的用户,但只有几个月(从未超过约24)。 SQL Server 2008. –

+0

我认为你的第三行应该有'cumulative_total = 6.5',而不是'8.5'。 –

回答

2

你在SQL Server 2008中的选择是合理的限制 - 你可以根据上面的方法做一些事情(称为'古怪的更新'),或者你可以在CLR中做一些事情。

就我个人而言,我会和CLR一起工作,因为它可以保证工作,而古怪的更新语法不是正式支持的(所以可能会在未来版本中打破)。

你正在寻找的古怪更新语法的变化会是这样的:

UPDATE my_table 
SET @CumulativeTotal=cumulative_total=ISNULL(total, 0) + 
     CASE WHEN @[email protected] THEN @CumulativeTotal ELSE 0 END, 
    @user=lastUser 

值得一提的是,在SQL Server 2012中引入了窗口函数RANGE支持,所以这是一种方式表达这是最高效的,同时得到100%的支持。

+0

非常好,那很完美 - 谢谢你Matt。对支持的关注以及关于RANGE的说明受到关注和赞赏。 –

+0

@Matt,正如我在回答结束时指出的那样,如果你已经在使用'RANGE',你应该测试'ROWS'产生相同的结果(它并不是在所有情况下),因为它*应该*更有效率。另外,您是否知道任何已发布的CLR解决方案都比本页面上的方法更快?我知道CLR对于某些事情来说太棒了(例如分割一个字符串),但是特别是在这个问题上更好吗?数学计算稍快一点?我希望CLR的开销超过了它的任何好处(但很高兴被证明是错误的)。 –

+0

@AaronBertrand - 是的,我应该明确地说出RANGE/ROWS,因为我倾向于将整个语法扩展看作一个屋檐下。我所说的CLR位是在一个线轴上进行计算,这很好。之前我在CLR上做过这样的事情,结果非常好。尝试并获得一些好的测量值将是一个有趣的练习。 –

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的解决方案,马特提到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

+0

谢谢Aaron,这非常有帮助。非常感激。 –