Itzik Ben Gan 的this document 是关于在 SQL Server 中计算运行总数的一个很好的资源,该资源作为他的活动的一部分提交给 SQL Server 团队,以进一步扩展其最初的 SQL Server 2005 实施中的OVER 子句.在其中他展示了一旦进入数万行游标如何执行基于集合的解决方案。 SQL Server 2012 确实扩展了OVER 子句,使这种查询变得更加容易。
SELECT col1,
SUM(col1) OVER (ORDER BY ind ROWS UNBOUNDED PRECEDING)
FROM @tmp
因为您使用的是 SQL Server 2005,但是您无法使用它。
Adam Machanic shows here 如何使用 CLR 来提高标准 TSQL 游标的性能。
对于这个表定义
CREATE TABLE RunningTotals
(
ind int identity(1,1) primary key,
col1 int
)
我在使用 ALLOW_SNAPSHOT_ISOLATION ON 的数据库中创建了包含 2,000 行和 10,000 行的表,其中一个设置为关闭(原因是我最初的结果是在一个设置为结果)。
所有表的聚集索引只有 1 个根页。每个叶子页面的数量如下所示。
+-------------------------------+-----------+------------+
| | 2,000 row | 10,000 row |
+-------------------------------+-----------+------------+
| ALLOW_SNAPSHOT_ISOLATION OFF | 5 | 22 |
| ALLOW_SNAPSHOT_ISOLATION ON | 8 | 39 |
+-------------------------------+-----------+------------+
我测试了以下案例(链接显示执行计划)
- Left Join and Group By
- 相关子查询2000 row plan,10000 row plan
- CTE from Mikael's (updated) answer
- CTE below
包含额外的 CTE 选项的原因是为了提供一个 CTE 解决方案,如果不能保证 ind 列是连续的,它仍然可以工作。
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
DECLARE @col1 int, @sumcol1 bigint;
WITH RecursiveCTE
AS (
SELECT TOP 1 ind, col1, CAST(col1 AS BIGINT) AS Total
FROM RunningTotals
ORDER BY ind
UNION ALL
SELECT R.ind, R.col1, R.Total
FROM (
SELECT T.*,
T.col1 + Total AS Total,
rn = ROW_NUMBER() OVER (ORDER BY T.ind)
FROM RunningTotals T
JOIN RecursiveCTE R
ON R.ind < T.ind
) R
WHERE R.rn = 1
)
SELECT @col1 =col1, @sumcol1=Total
FROM RecursiveCTE
OPTION (MAXRECURSION 0);
所有查询都添加了CAST(col1 AS BIGINT),以避免在运行时出现溢出错误。此外,对于所有这些,我将结果分配给上述变量,以消除在考虑中发送回结果所花费的时间。
结果
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| | | | Base Table | Work Table | Time |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| | Snapshot | Rows | Scan count | logical reads | Scan count | logical reads | cpu | elapsed |
| Group By | On | 2,000 | 2001 | 12709 | | | 1469 | 1250 |
| | On | 10,000 | 10001 | 216678 | | | 30906 | 30963 |
| | Off | 2,000 | 2001 | 9251 | | | 1140 | 1160 |
| | Off | 10,000 | 10001 | 130089 | | | 29906 | 28306 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| Sub Query | On | 2,000 | 2001 | 12709 | | | 844 | 823 |
| | On | 10,000 | 2 | 82 | 10000 | 165025 | 24672 | 24535 |
| | Off | 2,000 | 2001 | 9251 | | | 766 | 999 |
| | Off | 10,000 | 2 | 48 | 10000 | 165025 | 25188 | 23880 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| CTE No Gaps | On | 2,000 | 0 | 4002 | 2 | 12001 | 78 | 101 |
| | On | 10,000 | 0 | 20002 | 2 | 60001 | 344 | 342 |
| | Off | 2,000 | 0 | 4002 | 2 | 12001 | 62 | 253 |
| | Off | 10,000 | 0 | 20002 | 2 | 60001 | 281 | 326 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| CTE Alllows Gaps | On | 2,000 | 2001 | 4009 | 2 | 12001 | 47 | 75 |
| | On | 10,000 | 10001 | 20040 | 2 | 60001 | 312 | 413 |
| | Off | 2,000 | 2001 | 4006 | 2 | 12001 | 94 | 90 |
| | Off | 10,000 | 10001 | 20023 | 2 | 60001 | 313 | 349 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
相关子查询和GROUP BY 版本都使用由RunningTotals 表(T1) 上的聚集索引扫描驱动的“三角形”嵌套循环连接,并且对于该扫描返回的每一行,回溯到表 (T2) 在 T2.ind<=T1.ind 上自行加入。
这意味着重复处理相同的行。当T1.ind=1000 行被处理时,自连接检索所有行并将其与ind <= 1000 相加,然后对于T1.ind=1001 的下一行,再次检索相同的1000 行再次并与一个相加附加行等等。
对于 2,000 行的表,此类操作的总数为 2,001,000,对于 10k 行,通常为 50,005,000 或更多 (n² + n) / 2,这显然呈指数增长。
在 2,000 行的情况下,GROUP BY 和子查询版本之间的主要区别在于前者在连接之后具有流聚合,因此有三列馈入其中(T1.ind、T2.col1、@987654353 @) 和 T1.ind 的 GROUP BY 属性,而后者被计算为标量聚合,在连接之前使用流聚合,只有 T2.col1 馈入其中并且根本没有设置 GROUP BY 属性。可以看出,这种更简单的安排在减少 CPU 时间方面具有明显的好处。
对于 10,000 行的情况,子查询计划存在额外差异。它添加了一个eager spool,它将所有ind,cast(col1 as bigint) 值复制到tempdb。在快照隔离的情况下,它比聚集索引结构更紧凑,最终效果是减少了大约 25% 的读取次数(因为基表为版本信息保留了相当多的空白空间),当此选项关闭时,它会变得不那么紧凑(可能是由于bigint 与int 的差异)和更多的读取结果。这减少了子查询和按版本分组之间的差距,但子查询仍然获胜。
然而,明显的赢家是递归 CTE。对于“无间隙”版本,从基表读取的逻辑现在是 2 x (n + 1),反映了 n 索引搜索 2 级索引以检索所有行加上末尾的附加行,该行不返回任何内容并终止递归.然而,这仍然意味着要处理 22 页表需要 20,002 次读取!
递归 CTE 版本的逻辑工作表读取非常高。每个源行似乎可以读取 6 个工作表。这些来自存储前一行输出的索引假脱机,然后在下一次迭代中再次读取(Umachandar Jayachandran here 对此进行了很好的解释)。尽管数量众多,但它仍然是表现最好的。