Answers:
有关递归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)
查询计划为:
执行从计划的根部开始(SELECT),然后控制权将树向下传递到索引假脱机,并置,然后到达顶层表扫描。
扫描的第一行经过树,并被(a)存储在堆栈假脱机中,并且(b)返回给客户端。首先没有定义哪一行,但是为了论证,让我们假定它是值为{1}的行。因此,要显示的第一行是{1}。
控件再次向下传递到“表扫描”(“连接”运算符在打开下一个输入之前会消耗掉其最外部输入的所有行)。扫描发出第二行(值{2}),这再次将树向上传递以存储在堆栈上并输出到客户端。客户端现在已接收到序列{1},{2}。
通过采用LIFO堆栈顶部在左侧的约定,该堆栈现在包含{2,1}。当控件再次传递到“表扫描”时,它不再报告任何行,并且控件传递回“串联”运算符,该运算符打开了它的第二个输入(它需要一行才能传递到堆栈假脱机),并且控件传递给“内部联接”首次。
内部联接在其外部输入上调用表假脱机,该假脱机从堆栈{2}中读取第一行并将其从工作表中删除。堆栈现在包含{1}。
内部连接收到一行后,内部连接将控制权从其内部输入向下传递到左反半连接(LASJ)。这从其外部输入请求一行,将控制权传递给Sort。Sort是一个阻塞的迭代器,因此它从表变量中读取所有行,并对它们进行升序排序(发生时)。
因此,排序发出的第一行是值{1}。LASJ的内侧返回递归成员的当前值(该值刚刚从堆栈中弹出),即{2}。LASJ处的值为{1}和{2},因此发射{1},因为这些值不匹配。
该行{1}沿查询计划树向上流动到索引(堆栈)假脱机,在此它被添加到堆栈中,该堆栈现在包含{1,1},并发出给客户端。客户端现在已接收到序列{1},{2},{1}。
控制权现在返回到串联,从内侧向下(它上次返回一行,可能会再次执行),再通过内部联接向下到达LASJ。它再次读取其内部输入,从Sort中获得值{2}。
递归成员仍然是{2},因此这一次LASJ找到了{2}和{2},从而没有发出任何行。在其内部输入上不再找到任何行(排序现在不在行中),控件将传递回内部联接。
内部联接读取其外部输入,这会导致值{1}从堆栈{1,1}中弹出,而堆栈中只剩下{1}。现在,该过程重复进行,新的表扫描和排序调用中的值{2}通过LASJ测试并被添加到堆栈中,并传递给客户端,客户端现在已经收到{1},{2}, {1},{2} ...然后我们开始。
对于递归CTE计划中使用的Stack假脱机,我最喜欢的解释是Craig Freedman。
递归CTE的BOL描述描述了递归执行的语义,如下所示:
请注意,以上是逻辑说明。如此处所示,操作的物理顺序可能有所不同
将其应用于您的CTE,我期望使用以下模式进行无限循环
+-----------+---------+---+---+---+
| Invocation| Results |
+-----------+---------+---+---+---+
| 1 | 1 | 2 | 3 | |
| 2 | 4 | 5 | | |
| 3 | 1 | 2 | 3 | |
| 4 | 4 | 5 | | |
| 5 | 1 | 2 | 3 | |
+-----------+---------+---+---+---+
因为
select a
from cte
where a in (1,2,3)
是Anchor表达式。这显然返回1,2,3
为T0
此后,递归表达式运行
select a
from cte
except
select a
from r
使用1,2,3
as作为输入将产生4,5
as 的输出,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 |
+-----------+---------+---+---+---+
从使用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,1
3弹出堆栈,堆栈内容
2,1
LASJ返回
1,2,4,5
,堆栈内容5,4,2,1,2,1
5弹出堆栈,堆栈内容
4,2,1,2,1
LASJ返回
1,2,3,4
堆栈内容4,3,2,1,5,4,2,1,2,1
4弹出堆栈,堆栈内容
3,2,1,5,4,2,1,2,1
LASJ返回
1,2,3,5
堆栈内容5,3,2,1,3,2,1,5,4,2,1,2,1
5弹出堆栈,堆栈内容
3,2,1,3,2,1,5,4,2,1,2,1
LASJ返回
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
不允许这样做,并引发错误“子查询中不允许递归引用”。因此EXCEPT
在这种情况下甚至可以允许疏忽大意。
另外: Microsoft现在对我的“ 连接反馈”做出了如下响应
杰克的猜测是正确的:这应该是语法错误。
EXCEPT
子句中确实不应允许使用递归引用。我们计划在即将发布的服务版本中解决此错误。同时,我建议避免在EXCEPT
子句中使用递归引用。在限制递归方面,
EXCEPT
我们遵循ANSI SQL标准,该标准自引入递归以来就一直包含此限制(我相信在1999年)。关于EXCEPT
诸如SQL之类的声明性语言中递归的语义(也称为“非分层否定”),尚未达成广泛共识。另外,众所周知,在RDBMS系统中很难(如果不是不可能的话)有效地实现这种语义(对于合理大小的数据库)。
并且看起来最终实现是在2014年对兼容级别为120或更高的数据库进行的。
EXCEPT子句中的递归引用会生成符合ANSI SQL标准的错误。