递归CTE如何逐行运行?

48
我认为我已经掌握了递归CTE的格式,可以编写一个,但我仍然感到非常沮丧,因为我无法手动处理它(假装成SQL引擎自己并用笔和纸达到结果集)。我找到了这个, 它接近我想要的,但不够详细。我没有问题追踪C ++递归函数并理解它如何运行 - 但对于SQL,我不明白引擎为什么或如何知道何时停止。锚点和递归块每次都会被调用吗?还是在后面的迭代中跳过锚点?(我怀疑,但我试图表达我对它似乎跳来跳去的方式感到困惑。)如果每次都调用锚点,那么锚点如何不会出现多次在最终结果中?我希望有人能够逐行分解发生了什么以及随着结果集累积,“在内存中”的内容。

我已经借用了这个页面的示例, 因为它似乎是最容易理解的。

DECLARE @tbl TABLE ( 
      Id INT 
    , [Name] VARCHAR(20) 
    , ParentId INT 
) 

INSERT INTO @tbl( Id, Name, ParentId ) 
VALUES 
     (1, 'Europe', NULL) 
    ,(2, 'Asia',   NULL) 
    ,(3, 'Germany',   1) 
    ,(4, 'UK',        1) 
    ,(5, 'China',     2) 
    ,(6, 'India',     2) 
    ,(7, 'Scotland',  4) 
    ,(8, 'Edinburgh', 7) 
    ,(9, 'Leith',     8)  
; 

WITH abcd 
    AS ( 
        -- anchor 
        SELECT  id, Name, ParentID, 
                CAST(Name AS VARCHAR(1000)) AS Path 
        FROM    @tbl 
        WHERE   ParentId IS NULL 
        UNION ALL 
          --recursive member 
        SELECT  t.id, t.Name, t.ParentID, 
                CAST((a.path + '/' + t.Name) AS VARCHAR(1000)) AS "Path"
        FROM    @tbl AS t 
                JOIN abcd AS a 
                  ON t.ParentId = a.id 
       )
SELECT * FROM abcd 
5个回答

44

将递归的CTE视为无限的UNION ALL

WITH    rows AS
        (
        SELECT  *
        FROM    mytable
        WHERE   anchor_condition
        ),
        rows2 AS
        (
        SELECT  *
        FROM    set_operation(mytable, rows)
        ),
        rows3 AS
        (
        SELECT  *
        FROM    set_operation(mytable, rows2)
        ),
        …
SELECT  *
FROM    rows
UNION ALL
SELECT  *
FROM    rows2
UNION ALL
SELECT  *
FROM    rows3
UNION ALL

在您的情况下,应该是这样的:
WITH    abcd1 AS
        ( 
        SELECT  *
        FROM    @tbl t
        WHERE   ParentId IS NULL 
        ),
        abcd2 AS
        ( 
        SELECT  t.*
        FROM    abcd1
        JOIN    @tbl t
        ON      t.ParentID = abcd1.id
        ),
        abcd3 AS
        ( 
        SELECT  t.*
        FROM    abcd2
        JOIN    @tbl t
        ON      t.ParentID = abcd2.id
        ),
        abcd4 AS
        ( 
        SELECT  t.*
        FROM    abcd3
        JOIN    @tbl t
        ON      t.ParentID = abcd3.id
        ),
        abcd5 AS
        ( 
        SELECT  t.*
        FROM    abcd4
        JOIN    @tbl t
        ON      t.ParentID = abcd4.id
        ),
        abcd6 AS
        ( 
        SELECT  t.*
        FROM    abcd5
        JOIN    @tbl t
        ON      t.ParentID = abcd5.id
        )
SELECT  *
FROM    abcd1
UNION ALL
SELECT  *
FROM    abcd2
UNION ALL
SELECT  *
FROM    abcd3
UNION ALL
SELECT  *
FROM    abcd4
UNION ALL
SELECT  *
FROM    abcd5
UNION ALL
SELECT  *
FROM    abcd6

abcd6没有结果,这意味着它是一个停止条件。

理论上,递归的CTE可能是无限的,但实际上,SQL Server试图禁止导致无限记录集的查询。

您可能想阅读这篇文章:


1
很好的解释,Quassnoi! - Baaju
1
加1,我理解了那个引用。 - SyMo7amed

42

CTE使用的算法如下:

  1. 执行锚定部分,获取结果r0
  2. 执行递归部分,使用r0作为输入,并得到结果r1(非空)
  3. 执行递归部分,使用r1作为输入,并得到结果r2(非空)
  4. 执行递归部分,使用r2作为输入,并得到结果r3(非空) ...
  5. 执行递归部分,使用r(n-1)作为输入,并输出rn(空)。这时rn为空,因此我们使用UNION ALLr0、r1、r2 ... r(n-1)组合在一起,得到最终结果

让我们举个例子:

WITH    cte ( value )
          AS (
               SELECT   1
               UNION ALL
               SELECT   value + 1
               FROM     cte
               WHERE    value < 4
             )
    SELECT  *
    FROM    cte
这个查询的结果是:
value
-----------
1
2
3
4

(4 row(s) affected)

让我们逐步检查它:

Execute anchor query (SELECT 1), we got:
  r0 = 1
  cte = r0 = 1

    |
    |
    V

Now we execute
SELECT value + 1 FROM cte WHERE value < 4
Since cte is r0 (only has 1), we got:
  r1 = 2
  cte = r1 = 2

    |
    |
    V

Now we execute
SELECT value + 1 FROM cte WHERE value < 4
Since cte is r1 (only has 2), we got:
  r2 = 3
  cte = r2 = 3

    |
    |
    V

Now we execute
SELECT value + 1 FROM cte WHERE value < 4
Since cte is r2 (only has 3), we got:
  r3 = 4
  cte = r3 = 4

    |
    |
    V

Now we execute
SELECT value + 1 FROM cte WHERE value < 4
Since cte is r3 (only has 4), we got:
  r4 = NULL (because r3 (4) is equal to 4, not less than 4)
Now we stop the recursion!

    |
    |
    V

Let's calculate the final result:
R = r0 union all
    r1 union all
    r2 union all
    r3 union all
  = 1 union all
    2 union all
    3 union all
    4 union all
  = 1
    2
    3
    4

5
这表明它并不是保持一个运行中的集合,而是在每个递归层级上获得一个单独的结果集。因此,它不是 R1 = 1,然后 R2 =(1 union all R1 + 1)=(1,2),然后 R3 =(1 union all R2)=(1,1+1,2+1)=(1,2,3),等等。相反,它只查看使用上一个结果集,并过滤那个结果集,而不是整个联合。它只在最后全部联合。换句话说,它使用仅递归成员的输出作为输入,而不是将锚定的联合成员的输出与递归成员结合起来使用。 - Triynko

8
我认为它可以这样分解:
1. 执行锚定语句。这将给你一组结果,称为基础集或T0。 2. 执行递归语句,使用T0作为要执行查询的表。当查询CTE时,这会自动发生。 3. 如果递归成员返回一些结果,则创建一个新集T1。然后再次执行递归成员,使用T1作为输入,如果有任何结果则创建T2。 4. 第3步继续进行,直到不再生成任何结果,或者达到由MAX_RECURSION选项设置的最大递归次数。 此页面可能最好地解释了它。它详细介绍了CTE执行路径的逐步演示。

1
现在有我们三个人与那篇文章相关联了 :-) - Martin Smith

1

步骤1:

1 Europe NULL Europe
2 Asia   NULL Asia

步骤2:

1 Europe  NULL Europe
2 Asia    NULL Asia
3 Germany 1    Europe/Germany
4 UK      1    Europe/UK
5 China   2    Asia/China
6 India   2    Asia/India

步骤三:

1 Europe   NULL Europe
2 Asia     NULL Asia
3 Germany  1    Europe/Germany
4 UK       1    Europe/UK
5 China    2    Asia/China
6 India    2    Asia/India
7 Scotland 4    Europe/UK/Scotland

步骤4:

1 Europe    NULL Europe
2 Asia      NULL Asia
3 Germany   1    Europe/Germany
4 UK        1    Europe/UK
5 China     2    Asia/China
6 India     2    Asia/India
7 Scotland  4    Europe/UK/Scotland
8 Edinburgh 7    Europe/UK/Scotland/Edinburgh

步骤5:

1 Europe    NULL Europe                             0
2 Asia      NULL Asia                               0
3 Germany   1    Europe/Germany                     1
4 UK        1    Europe/UK                          1
5 China     2    Asia/China                         1
6 India     2    Asia/India                         1
7 Scotland  4    Europe/UK/Scotland                 2
8 Edinburgh 7    Europe/UK/Scotland/Edinburgh       3
9 Leith     8    Europe/UK/Scotland/Edinburgh/Leith 4

第5步中的最后一列是级别。在每个级别中,行会根据已有的内容进行添加。希望这可以帮到您。

1

你可能想要这个链接。不,锚点不会被执行多次(否则union all将要求所有结果都出现)。详见上一个链接。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接