sql-server – 在递归公用表表达式中使用EXCEPT

前端之家收集整理的这篇文章主要介绍了sql-server – 在递归公用表表达式中使用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

我试图在Stack Overflow上回答question时遇到了这个问题.

解决方法

有关递归CTE中EXCEPT的当前状态的信息,请参阅 Martin Smith’s answer.

解释你所看到的,以及为什么:

我在这里使用表变量,以区分锚值和递归项更清晰(它不会改变语义).

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}.当控件再次传递给表扫描时,它不再报告行,并且控制传递回连接操作符,这将打开它的第二个输入(它需要一行传递到堆栈线轴),并且控制传递给内部连接首次.

Inner join在其外部输入上调用Table Spool,它从堆栈{2}读取顶行并从工作表中删除它.堆栈现在包含{1}.

在其外部输入上接收到一行后,内部连接将其内部输入控制向左反连接(LASJ).这从其外部输入请求一行,将控制传递给Sort. Sort是一个阻塞迭代器,因此它从表变量读取所有行并对它们进行升序排序(当它发生时).

因此Sort发出的第一行是值{1}. LASJ的内侧返回递归成员的当前值(刚从堆栈中弹出的值),即{2}. LASJ的值为{1}和{2},因此值为{1},因为值不匹配.

此行{1}将查询计划树向上流向索引(堆栈)假脱机,并将其添加到堆栈(现在包含{1,1})并发送到客户端.客户端现已收到序列{1},{2},{1}.

Control现在返回到Concatenation,从内侧返回(它最后一次返回一行,可能再次返回),然后通过Inner Join返回到LASJ.它再次读取其内部输入,从Sort中获取值{2}.

递归成员仍然是{2},所以这次LASJ找到{2}和{2},导致没有发出行.在其内部输入上找不到更多行(Sort现在不在行中),控件将返回到内部连接.

内部连接读取其外部输入,这导致值{1}从堆栈{1,1}弹出,使堆栈仅为{1}.现在重复该过程,来自Table Scan和Sort的新调用的值{2}通过LASJ测试并被添加到堆栈,并传递给客户端,该客户端现已收到{1},{1},{2} ……我们走了.

我最喜欢的递归CTE计划中使用的堆栈线轴explanation是Craig Freedman的.

猜你在找的MsSQL相关文章