递归 CTE 导致缓慢和索引扫描

Posted

技术标签:

【中文标题】递归 CTE 导致缓慢和索引扫描【英文标题】:Recursive CTE causes slowness and index scan 【发布时间】:2017-08-31 09:12:32 【问题描述】:

我有一个包含位置信息的表 (Location_Tree),以 Id/ParentId 结构排列在多个级别上,从“世界”的第 0 级一直到各个城市,通过世界地区、国家、州、县、州和城市。我们有另一个事件表,其中有一列“CreatedWithLocationId”,表示事件的位置,但没有说明所提供的位置是城市、县、国家还是其他 - 换句话说,准确性是不确定的。

我正在使用查询来制作一份报告,该报告获取一个事件的位置并显示“附近”列出的其他事件。最佳匹配将是与输入位置共享位置的那些,但如果找不到足够的匹配,我将“缩小”该位置,在树上搜索父位置,直到找到足够的匹配报告要求.

为了实现这一点,我做了一个递归 CTE,它将从 Location_Tree 表中获取给定位置的所有子节点。因此,我获取输入位置,找到 parentId,然后找到共享该 parentId 的所有位置,或者将其作为祖父母等,根据需要按级别限制等等。

DECLARE @Ancestor_Id INT = 1;

WITH Location_Children AS
(
SELECT @Ancestor_Id AS Id, NULL AS ParentId
UNION ALL
 SELECT
    B.Id, B.ParentId
 FROM       Location_Tree       AS B
 INNER JOIN Location_Children   AS C    ON B.ParentId = C.Id
)

SELECT * FROM Location_Children;

即使返回的唯一行是@Ancestor_Id AS Id, NULL AS ParentId 行,上述查询总是会导致对Location_Tree 表() 的主键的聚集索引扫描和急切假脱机——所有大量行参与执行计划,查询大约需要15秒完成,返回我的一行。

--> Execution Plan

有人对我如何加快速度有任何建议吗?我已经尝试添加索引等,我有点不愿意进行大规模查询以使用 CASE 语句或一系列左连接来替换 CTE,因为这需要大量的脚本编写......我已经尝试了这个查询的每一种排列方式,使用内联函数、自定义表数据类型、(几乎)一切都无济于事......接下来我应该尝试什么?

【问题讨论】:

您的执行计划似乎与您的示例代码不匹配。使用Paste The Plan @ brentozar.com 分享您的执行计划,以下是说明:How to Use Paste the Plan。 @SqlZim 感谢您提供的信息;我已更新问题以包含新创建的链接。 如果你能告诉我 Eager Spool 的成本百分比,我或许可以回答这个问题。前几天我在递归 CTE 中遇到了类似的问题,不得不寻找涉及该运算符的解决方法,结果证明这是罪魁祸首。 @SQLServerSteve - Eager Spool 成本是执行计划的 72%,而聚集索引扫描是另外 28% 感谢@High Plains Grifter - 我错过了上面指向您的执行计划的链接。 【参考方案1】:

如果 Eager Spool 消耗了这么多查询成本,那么查看 Itzik Ben-Gan 在 Divide and Conquer Halloween: Avoid the Overhead of Halloween Protection 上的帖子可能会有所启发。万圣节问题发生在诸如涉及非聚集索引的 UPDATE 语句之类的情况下,其中查询优化器可能会引用与更新值不同步的索引值,从而导致不正确的数据甚至无限循环。 Eager Spool 是 SQL Server 查询优化器在万圣节保护等情况下使用的阻塞运算符。在他关于万圣节问题的excellent four-part series(Ben-Gan 也链接到)中,Paul White 解释了对 Eager Spools 的需求:

"没有提示或跟踪标志来防止将假脱机包含在 这个执行计划,因为它是正确性所必需的。就像它一样 顾名思义,线轴急切地消耗其子级的所有行 在将行返回到其父 Compute 之前的运算符(Index Seek) 标量。这样做的效果是引入了完全的相分离—— 之前读取所有符合条件的行并将其保存到临时存储中 执行任何更新。”

递归 CTE 有时也需要它们,它更新内部工作表(在 TempDB 中,如果我没记错的话)。大多数消息来源说你基本上被他们困住了。通过彻底索引连接中涉及的所有列,我已经能够删除昂贵的 Eager Spools,但生成的执行计划迄今为止并没有更快,并且表现出几乎拜占庭式的复杂性;性能损失只是转移到了同样昂贵的 Concatenate 运算符。一些消息来源以一种随意的方式暗示 WITH (NOLOCK) 或 WITH (READ UNCOMMITTED) 可能会有所帮助,但我对这些没有任何运气;它们每次都会产生完全相同的执行计划,而昂贵的 Eager Spools 仍然存在。我不想抄袭 Ben-Gan 的代码,特别是因为我还没有尝试过,但他的解决方法是完全放弃递归 CTE 方法,并对一对临时表或表变量进行递归更新,以欺骗优化器删除万圣节保护机制。几天前,我在一对关键的长时间运行的查询中遇到了这个问题,但从未成功删除 Eager Spools;我只能通过从查询和其他此类优化中无情地清除不必要的重复项来让它们运行,所有这些都需要数小时的实验以及在递归 CTE 的连接中添加许多复杂的 CASE 语句以减少正在处理的不必要的行数。我正在处理的查询是生成递归 CTE,它创建了数据的连续排列;在某些时候,我可以通过预先计算识别排列的数字来降低总体计算成本,但这可能不适用于您的情况,即父子检索 CTE。

我希望我能更令人鼓舞,但无论如何,您可能会被昂贵的阻塞 Eager Spool 卡住,除非您使用像 Ben-Gan 这样的彻底放弃递归 CTE 方法的解决方法。如果您想保持递归 CTE 方法,您唯一的选择可能是提供复杂的逻辑,以减少正在处理的不必要行的数量。在连接条件上使用非聚集索引也可能会带来一点好处,但基本上您正在查看一系列可能很耗时的零碎优化。

【讨论】:

以上是关于递归 CTE 导致缓慢和索引扫描的主要内容,如果未能解决你的问题,请参考以下文章

SQL Server CTE 递归查询全解

SQL Server CTE 和递归示例

表数据子集上的递归 CTE

MYSQL 8.019 CTE 递归查询怎么解决死循环三种方法

SQLServer CTE递归和循环

递归 CTE 通过多个级别更新父记录