在 SQL Server 中计算运行总计的一个很好的资源是Itzik Ben Gan 的这份文档,该文档作为他的活动的一部分提交给 SQL Server 团队,以OVER
进一步扩展从其初始 SQL Server 2005 实施的子句。在其中他展示了一旦进入数万行游标如何执行基于集合的解决方案。SQL Server 2012 确实扩展了该OVER
子句,使这种查询变得更加容易。
SELECT col1,
SUM(col1) OVER (ORDER BY ind ROWS UNBOUNDED PRECEDING)
FROM @tmp
由于您使用的是 SQL Server 2005,但是您无法使用此功能。
Adam Machanic在这里展示了如何使用 CLR 来提高标准 TSQL 游标的性能。
对于这个表定义
CREATE TABLE RunningTotals
(
ind int identity(1,1) primary key,
col1 int
)
我在数据库中创建了包含 2,000 行和 10,000 行的表,ALLOW_SNAPSHOT_ISOLATION ON
其中一个关闭了此设置(原因是我的初始结果位于数据库中,该设置导致结果令人费解)。
所有表的聚集索引只有 1 个根页。每个叶子页的数量如下所示。
+-------------------------------+-----------+------------+
| | 2,000 row | 10,000 row |
+-------------------------------+-----------+------------+
| ALLOW_SNAPSHOT_ISOLATION OFF | 5 | 22 |
| ALLOW_SNAPSHOT_ISOLATION ON | 8 | 39 |
+-------------------------------+-----------+------------+
我测试了以下案例(链接显示执行计划)
- 左连接和分组依据
- 相关子查询 2000 行计划,10000 行计划
- 来自 Mikael(更新)答案的 CTE
- CTE 下面
包含附加 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
, T2.col1
) 和GROUP BY
属性 ,T1.ind
而后者被计算为标量聚合,在连接之前使用流聚合,只有T2.col1
馈入它并且根本没有GROUP BY
设置任何属性。可以看出,这种更简单的安排在减少 CPU 时间方面具有可衡量的好处。
对于 10,000 行的情况,子查询计划存在额外差异。它添加了一个急切的线轴ind,cast(col1 as bigint)
,它将所有值复制到tempdb
. 在快照隔离的情况下,它比聚集索引结构更紧凑,净效果是减少约 25% 的读取次数(因为基表为版本信息保留了相当多的空白空间),当此选项关闭时,它会变得不那么紧凑(可能是由于bigint
vsint
差异)和更多的读取结果。这减少了子查询和按版本分组之间的差距,但子查询仍然获胜。
然而,明显的赢家是递归 CTE。对于“无间隙”版本,来自基表的逻辑读取现在2 x (n + 1)
反映了n
索引搜索到 2 级索引以检索所有行以及末尾的附加行,该行不返回任何内容并终止递归。然而,这仍然意味着要处理 22 页表需要 20,002 次读取!
递归 CTE 版本的逻辑工作表读取非常高。每个源行似乎可以读取 6 个工作表。这些来自存储前一行输出的索引假脱机,然后在下一次迭代中再次读取(Umachandar Jayachandran 对此进行了很好的解释)。尽管数量众多,但它仍然是表现最好的。