Tom*_*ter 33 sql-server-2008 sql-server execution-plan recursive except
为什么以下查询返回无限行?我本来希望该EXCEPT条款终止递归..
with cte as (
select *
from (
values(1),(2),(3),(4),(5)
) v (a)
)
,r as (
select a
from cte
where a in (1,2,3)
union all
select a
from (
select a
from cte
except
select a
from r
) x
)
select a
from r
Run Code Online (Sandbox Code Playgroud)
Mar*_*ith 32
递归 CTE 的 BOL 描述描述了递归执行的语义如下:
请注意,以上是逻辑描述。操作的物理顺序可能有所不同,如下所示
将此应用到您的 CTE 我希望有以下模式的无限循环
+-----------+---------+---+---+---+
| Invocation| Results |
+-----------+---------+---+---+---+
| 1 | 1 | 2 | 3 | |
| 2 | 4 | 5 | | |
| 3 | 1 | 2 | 3 | |
| 4 | 4 | 5 | | |
| 5 | 1 | 2 | 3 | |
+-----------+---------+---+---+---+
Run Code Online (Sandbox Code Playgroud)
因为
select a
from cte
where a in (1,2,3)
Run Code Online (Sandbox Code Playgroud)
是锚表达式。这显然返回1,2,3为T0
此后递归表达式运行
select a
from cte
except
select a
from r
Run Code Online (Sandbox Code Playgroud)
使用1,2,3as 将产生4,5as输出的输入,T1然后将其插入下一轮递归将1,2,3无限期地返回,依此类推。
然而,这并不是实际发生的情况。这些是前 5 次调用的结果
+-----------+---------+---+---+---+
| Invocation| Results |
+-----------+---------+---+---+---+
| 1 | 1 | 2 | 3 | |
| 2 | 1 | 2 | 4 | 5 |
| 3 | 1 | 2 | 3 | 4 |
| 4 | 1 | 2 | 3 | 5 |
| 5 | 1 | 2 | 3 | 4 |
+-----------+---------+---+---+---+
Run Code Online (Sandbox Code Playgroud)
从使用OPTION (MAXRECURSION 1)和增量向上调整1可以看出,它进入一个循环,每个连续级别将在输出1,2,3,4和 之间不断切换1,2,3,5。
正如@Quassnoi在这篇博文中所讨论的那样。结构的观察结果的模式,就好像每个调用都在做(1),(2),(3),(4),(5) EXCEPT (X),其中X是从以前调用的最后一行。
编辑:在阅读SQL Kiwi 的优秀答案后,很清楚为什么会发生这种情况,而且这不是故事的全部,因为堆栈中仍然有很多东西永远无法处理。
锚定发射
1,2,3到客户端堆栈内容3,2,13 弹出堆栈,堆栈内容
2,1LASJ 返回
1,2,4,5,堆栈内容5,4,2,1,2,15 弹出堆栈,堆栈内容
4,2,1,2,1LASJ 返回
1,2,3,4堆栈内容4,3,2,1,5,4,2,1,2,14 弹出堆栈,堆栈内容
3,2,1,5,4,2,1,2,1LASJ 返回
1,2,3,5堆栈内容5,3,2,1,3,2,1,5,4,2,1,2,15 弹出堆栈,堆栈内容
3,2,1,3,2,1,5,4,2,1,2,1LASJ 返回
1,2,3,4堆栈内容4,3,2,1,3,2,1,3,2,1,5,4,2,1,2,1
如果您尝试用逻辑上等价的(在没有重复/NULL 的情况下)表达式替换递归成员
select a
from (
select a
from cte
where a not in
(select a
from r)
) x
Run Code Online (Sandbox Code Playgroud)
这是不允许的,并会引发错误“子查询中不允许递归引用”。所以也许EXCEPT在这种情况下甚至允许这是一种疏忽。
补充: 微软现在对我的Connect 反馈做出如下回应
Jack的猜测是正确的:这应该是语法错误;确实不应该在
EXCEPT子句中允许递归引用。我们计划在即将发布的服务版本中解决此错误。同时,我建议避免在EXCEPT子句中递归引用。在限制递归时,
EXCEPT我们遵循 ANSI SQL 标准,自从引入递归以来(我相信是 1999 年),该标准就包含了此限制。对于EXCEPTSQL 等声明性语言中的递归(也称为“非分层否定”)的语义应该是什么,还没有广泛的共识。此外,众所周知,在 RDBMS 系统中(对于合理大小的数据库)有效地实现这种语义是非常困难的(如果不是不可能的话)。
看起来最终实现是在 2014 年针对兼容级别为 120 或更高的数据库进行的。
EXCEPT 子句中的递归引用会生成符合 ANSI SQL 标准的错误。
Pau*_*ite 27
有关递归 CTE 中的当前状态的信息,请参阅Martin Smith 的回答EXCEPT。
解释您所看到的内容以及原因:
我在这里使用了一个表变量,以使锚值和递归项之间的区别更清晰(它不会改变语义)。
DECLARE @V TABLE (a INTEGER NOT NULL)
INSERT @V (a) VALUES (1),(2)
;
WITH rCTE AS
(
-- Anchor
SELECT
v.a
FROM @V AS v
UNION ALL
-- Recursive
SELECT
x.a
FROM
(
SELECT
v2.a
FROM @V AS v2
EXCEPT
SELECT
r.a
FROM rCTE AS r
) AS x
)
SELECT
r2.a
FROM rCTE AS r2
OPTION (MAXRECURSION 0)
Run Code Online (Sandbox Code Playgroud)
查询计划是:

执行从计划的根(SELECT)开始,控制将树向下传递到索引假脱机、串联,然后传递到顶级表扫描。
扫描的第一行向上传递树并(a)存储在堆栈假脱机中,(b)返回给客户端。没有定义第一行,但为了论证,让我们假设它是值为 {1} 的行。因此,出现的第一行是 {1}。
控制再次传递到表扫描(连接运算符在打开下一个之前消耗其最外层输入中的所有行)。扫描发出第二行(值 {2}),这再次向上传递树以存储在堆栈上并输出到客户端。客户端现在已收到序列 {1}, {2}。
采用 LIFO 堆栈顶部在左侧的约定,堆栈现在包含 {2, 1}。当控制再次传递给表扫描时,它不再报告更多的行,并且控制传递回连接运算符,后者打开它的第二个输入(它需要一个行传递给堆栈假脱机),并将控制传递给内部联接首次。
内连接在其外部输入上调用表假脱机,它从堆栈 {2} 中读取顶行并将其从工作表中删除。堆栈现在包含 {1}。
在其外部输入上收到一行后,内部连接将控制权向下传递给左反半连接 (LASJ)。这从其外部输入请求一行,将控制权传递给 Sort。Sort 是一个阻塞迭代器,因此它从 table 变量中读取所有行并按升序对它们进行排序(碰巧)。
因此,Sort 发出的第一行是值 {1}。LASJ 的内侧返回递归成员的当前值(刚刚从堆栈中弹出的值),即 {2}。LASJ 处的值是 {1} 和 {2},因此发出 {1},因为这些值不匹配。
这一行 {1} 沿着查询计划树向上流动到索引(堆栈)假脱机,在那里它被添加到堆栈中,现在包含 {1, 1},并发送到客户端。客户端现在已收到序列 {1}、{2}、{1}。
控制现在传回串联,回到内侧(上次返回一行,可能会再次执行),通过内部联接向下传递到 LASJ。它再次读取其内部输入,从 Sort 获得值 {2}。
递归成员仍然是 {2},因此这次 LASJ 找到 {2} 和 {2},导致没有发出任何行。在其内部输入上找不到更多行(Sort 现在没有行了),控制权返回到 Inner Join。
Inner Join 读取其外部输入,这导致值 {1} 从堆栈 {1, 1} 中弹出,仅留下 {1} 堆栈。该过程现在重复,来自表扫描和排序的新调用的值 {2} 通过 LASJ 测试并被添加到堆栈,并传递给客户端,客户端现在已收到 {1}、{2}、 {1}、{2}……然后我们继续。
对于递归 CTE 计划中使用的堆栈线轴,我最喜欢的解释是 Craig Freedman 的。
| 归档时间: |
|
| 查看次数: |
10238 次 |
| 最近记录: |