【问题标题】:Numbers table vs recursive CTE to generate a range of numbers数字表与递归 CTE 生成一系列数字
【发布时间】:2022-01-26 16:35:13
【问题描述】:

为什么使用数字表比使用递归 CTE 动态生成它们要快得多?

在我的机器上,给定一个表numbers,其中包含一列n(主键),其中包含从1 到100000 的数字,以下查询:

select n from numbers;

大约需要 400 毫秒才能完成。

使用递归 CTE 生成数字 1 到 100000:

with u as (
    select 1 as n
    union all
    select n + 1
    from u
    where n < 100000
)
select n
from u
option(maxrecursion 0);

在 SQL Server 2019 上都需要大约 900 毫秒才能完成。

我的问题是,为什么第二个选项比第一个慢很多?第一个不是从磁盘获取结果,因此应该更慢吗?

否则,有什么方法可以让 CTE 运行得更快?因为在我看来,这是一个比在数据库中存储数字列表更优雅的解决方案。

【问题讨论】:

  • 简单,SQL Server 引擎针对基于集合的操作进行了优化,而不是递归操作。
  • @shawnt00 将第二个示例编辑为 u 而不是 numbers 以避免混淆。我试过了,它仍然需要同样的时间。
  • 我总是想知道为什么人们反对创建表 - 存储需求几乎立即收回成本,因为如果表被足够频繁地读取(而且应该如此!)它总是在内存中。内存相当快。另见this question
  • 在实用程序数据库中存储几百个 8k 页面,以供服务器上多个数据库上的任意数量查询重复使用,并且可以在几毫秒内读取,而 CPU 可忽略不计,而不是重复相同的昂贵 CPU 周期在每次调用时生成相同的列表,尤其是在为云中的 CPU 收费时....我知道我认为最小的方法!
  • 更多我自己对数字表值的评论:part 1 | part 2

标签: sql sql-server tsql


【解决方案1】:

递归 CTE 是一项消耗 CPU 资源的操作,因为 SQL Server 会“循环”覆盖行。物化数字表或基于集合的 CTE 将执行得更快。请注意在我的工作站 (YMMV) 上使用 SET STATISTICS TIME ON 报告的 CPU 和运行时间。

数字表:

SELECT * FROM dbo.numbers;

 SQL Server Execution Times:
   CPU time = 16 ms,  elapsed time = 231 ms.

递归 CTE:

with u as (
    select 1 as n
    union all
    select n + 1
    from u
    where n < 100000
)
select n
from u
option(maxrecursion 0);

 SQL Server Execution Times:
   CPU time = 375 ms,  elapsed time = 529 ms.

基于集合的 CTE:

WITH 
     t10 AS (SELECT n FROM (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) t(n))
    ,t1k AS (SELECT 0 AS n FROM t10 AS a CROSS JOIN t10 AS b CROSS JOIN t10 AS c)
    ,t100k AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS n FROM t1k AS a CROSS JOIN t10 AS b CROSS JOIN t10 AS c)
SELECT n
FROM t100k;

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 223 ms.

数字表的一个优点是可以利用唯一索引来优化某些查询,尽管此处不适用。

【讨论】:

  • 是什么让你说它的 CPU 比 IO 贵?我的印象是它需要从工作表中插入和读取行,即使删除了急切的写入(因此只写入内存),这仍然是导致速度下降的主要原因
  • @Charlieface,由于 CPU 昂贵,我指的是 Martin 在他的回答中详细说明的所有计算成本(即其他物理 IO),包括逻辑 IO。最初的问题是磁盘 IO,我将其解释为来自存储的物理 IO。
【解决方案2】:

当您将一列声明为主键时,SQL Server 会创建一个聚集索引,这意味着如果您使用主键查询该表,SQL Server 查询优化器会准确地知道该列的位置。换句话说,此查询尽可能高效,具有最少的 CPU 时间和逻辑读取。 要查看这两种方法之间差异的详细视图,请启用执行计划 WITH Ctrl+m 运行查询并比较两个执行计划。

【讨论】:

  • “当你声明一个列作为主键时,SQL Server 会创建一个聚集索引”默认是,但前提是你不改变它。这并不能解释为什么递归 CTE 会更慢。
  • 我不明白你所说的“但前提是你不改变它。”这解释了为什么从表中选择并按具有索引的列进行筛选具有最少的 io 和 CPU 时间。与消耗更多 CPU 功率的 cte 相比。
  • 我的意思是您不必将主键创建为聚集索引...它是可选的。
【解决方案3】:

不是第一个从磁盘获取结果的,因此应该 慢一点?

100,000 个整数将适合大约 161 个数据页(假设未使用压缩) - 每行将是 11 个字节,并在插槽数组中占用 2 个字节。

当您运行测试时,数据可能已经在缓存中。即使没有在缓存中,也很可能几乎所有页面在需要之前都已通过预读机制读入缓存,因此 IO 等待是最小的,并且它再次只是一个 CPU 绑定操作。 (您可以使用SET STATISTICS IO ON 查看实际需要多少物理读取和预读)

从缓存中的数据页读取行当然是 SQL Server 擅长的。从执行计划的角度来看,根本没有复杂性。正确的行可以从索引查找运算符(理想情况下或扫描运算符)返回,并直接输出到客户端,无需额外的运算符。

递归 CTE 功能是一种通用方法,它始终使用基本相同的执行计划。来自锚部分的行被添加到堆栈假脱机,然后从假脱机弹出(删除)以馈送到嵌套循环运算符,该运算符计算其内部子树上的递归部分并将值向上传递到执行计划树到被添加到堆栈假脱机(用于进一步递归)并返回给客户端。

所有这些执行计划操作都需要时间。我在本地机器上尝试了高达10,000,000 的数字。总体查询持续时间为 2m 6s(其中 38 秒花费在 9,999,999 PAGELATCH_SHtempdb 中等待假脱机)

这里的lazy latches section 描述了这些闩锁等待的原因。表假脱机操作员持有页面上的锁存器,但是当索引假脱机操作员尝试插入下一个数字的行(在同一个基础假脱机中)时,它被另一个操作员阻止。所以必须进入等待状态,释放锁存器并自行解除阻塞。 (特别是在IndexDataSetSession::LocatePageForInsert 下,这大概解释了为什么它在SH 模式下等待而不是EX)。在这种情况下,它们的数量相对较多,因为返回的每个数字都处于不同的递归级别,因此在再次调用 table spool 以重播该行之前,所有插入只执行一行。

您可以在下面看到“每个操作员”的时间安排。表假脱机操作员(节点 6)花费了可观的时间,主要是因为它每次发出一个时都会从假脱机中删除行。节点 0 是向假脱机插入行的运算符。基本上每个返回的数字都会经历这个等待/插入/删除循环(尽管由锚语句插入到假脱机的单个初始行可以在不等待的情况下这样做)

当然,可以提供像 Postgres generate_series 这样的功能,它完全基于 CPU,专门用于提供递增值的任务,这可能优于基于磁盘的表格方法,但目前还没有这样的专用功能已在产品中实现。在此之前,目前在没有数字表的情况下生成数字的“最先进”方法可能是this page 中第一次提到的方法。

【讨论】:

  • 这非常有用!我有兴趣了解您使用的按功能时间分解的程序
  • @GeorgeJoseph 这是将 Visual Studio 调试器附加到我的本地 SQL Server(已将 VS 配置为使用 Microsoft Public 符号服务器),然后在查询运行时使用 CPU 分析选项。也可以为此使用 Windows 性能记录器
  • 当 Postgres 用户需要一系列数字时,generate_series 通常是转到选项吗?
  • 我不知道。 @扎卡利亚。我几乎只使用 SQL Server,所以不知道在某些情况下这不是最佳选择
  • Erland Sommarskog 大约 12 年前就该主题开设了一个“连接”项目。尽管我们中的一些人给出了实际的例子,但 MS 的回复似乎表明他们不知道这样的东西会被用来做什么。该项目通过原始“连接”最终登陆 GIT 所经历的所有不同平台保持开放,甚至没有建议他们这样做。如果他们这样做了,只能希望他们不要像原来的 STRING_SPLIT() 或 FORMAT 的性能问题那样搞砸了。
猜你喜欢
  • 1970-01-01
  • 2014-04-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-16
  • 1970-01-01
  • 1970-01-01
  • 2013-07-25
相关资源
最近更新 更多