在递归公用表表达式中使用EXCEPT


33

为什么以下查询返回无限行?我本来希望该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

我在尝试回答有关堆栈溢出的问题时遇到了这个问题

Answers:


26

有关递归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)

查询计划为:

递归CTE计划

执行从计划的根部开始(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。


31

递归CTE的BOL描述描述了递归执行的语义,如下所示:

  1. 将CTE表达式分为锚成员和递归成员。
  2. 运行锚成员,以创建第一个调用或基本结果集(T0)。
  3. 使用Ti作为输入和Ti + 1作为输出运行递归成员。
  4. 重复步骤3,直到返回空集。
  5. 返回结果集。这是T0至Tn的UNION ALL。

请注意,以上是逻辑说明。如此处所示,操作的物理顺序可能有所不同

将其应用于您的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,3T0

此后,递归表达式运行

select a
from cte
except
select a
from r

使用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 |
+-----------+---------+---+---+---+

从使用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标准的错误。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.