SQL递归实际上如何工作?


19

来自其他编程语言的SQL,递归查询的结构看起来很奇怪。一步一步地走,它似乎崩溃了。

考虑以下简单示例:

CREATE TABLE #NUMS
(N BIGINT);

INSERT INTO #NUMS
VALUES (3), (5), (7);

WITH R AS
(
    SELECT N FROM #NUMS
    UNION ALL
    SELECT N*N AS N FROM R WHERE N*N < 10000000
)
SELECT N FROM R ORDER BY N;

让我们来看一看。

首先,执行锚成员并将结果集放入R。因此R初始化为{3,5,7}。

然后,执行降到UNION ALL以下,并且第一次执行递归成员。它在R上执行(即在我们当前拥有的R上:{3,5,7})。结果为{9,25,49}。

这个新结果如何处理?是否将{9,25,49}附加到现有的{3,5,7}上,标记结果并集R,然后从那里进行递归?还是将R重新定义为仅此新结果{9,25,49},然后再进行所有合并?

两种选择都没有道理。

如果R现在为{3,5,7,9,25,49}并且执行递归的下一个迭代,那么我们将以{9,25,49,81,625,2401}结束,并且失去了{3,5,7}。

如果现在R仅是{9,25,49},则存在标签错误的问题。R被理解为锚定成员结果集与所有后续递归成员结果集的并集。而{9,25,49}只是R的一个组成部分。到目前为止,我们还没有获得R的全部。因此,将递归成员写为从R中选择是没有意义的。


我当然感谢@Max Vernon和@Michael S.在下面进行了详细介绍。即,(1)创建所有组件直到递归限制或空集,然后(2)将所有组件结合在一起。这就是我理解SQL递归实际工作的方式。

如果我们正在重新设计SQL,也许我们将强制使用更清晰明了的语法,如下所示:

WITH R AS
(
    SELECT   N
    INTO     R[0]
    FROM     #NUMS
    UNION ALL
    SELECT   N*N AS N
    INTO     R[K+1]
    FROM     R[K]
    WHERE    N*N < 10000000
)
SELECT N FROM R ORDER BY N;

有点像数学中的归纳证明。

目前,SQL递归的问题在于它以一种令人困惑的方式编写。编写方式说每个组件都是通过从R中选择来形成的,但这并不意味着到目前为止已经(或似乎已经)构造了完整的R。它仅表示先前的组件。


“如果R现在为{3,5,7,9,25,49},并且我们执行了递归的下一个迭代,那么我们将以{9,25,49,81,625,2401}结束,我们将失去了{3,5,7}。” 如果这样做,我看不到您怎么输{3,5,7}。
ypercubeᵀᴹ

@yper-crazyhat-c​​ubeᵀᴹ—我遵循的是我提出的第一个假设,即,如果中间R是到那时为止已计算的所有事物的累加,该怎么办?然后,在递归成员的下一次迭代中,R的每个元素都平方。因此,{3,5,7}变为{9,25,49},我们不可能再有{3,5,7}中R.换句话说,{3,5,7}的自R.丢失
UnLogicGuys

Answers:


26

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

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

因此,每个级别仅具有输入级别,而不是到目前为止累积的整个结果集。

以上是它在逻辑上的工作方式。当前,物理递归CTE始终在SQL Server中使用嵌套循环和堆栈后台处理程序实现。这是这里描述这里和手段,在实践中每次递归元素只是与父工作从以前的水平,而不是整体水平。但是递归CTE中对允许语法的各种限制意味着该方法有效。

如果ORDER BY从查询中删除,则结果按以下顺序排序

+---------+
|    N    |
+---------+
|       3 |
|       5 |
|       7 |
|      49 |
|    2401 |
| 5764801 |
|      25 |
|     625 |
|  390625 |
|       9 |
|      81 |
|    6561 |
+---------+

这是因为执行计划的操作与以下内容非常相似 C#

using System;
using System.Collections.Generic;
using System.Diagnostics;

public class Program
{
    private static readonly Stack<dynamic> StackSpool = new Stack<dynamic>();

    private static void Main(string[] args)
    {
        //temp table #NUMS
        var nums = new[] { 3, 5, 7 };

        //Anchor member
        foreach (var number in nums)
            AddToStackSpoolAndEmit(number, 0);

        //Recursive part
        ProcessStackSpool();

        Console.WriteLine("Finished");
        Console.ReadLine();
    }

    private static void AddToStackSpoolAndEmit(long number, int recursionLevel)
    {
        StackSpool.Push(new { N = number, RecursionLevel = recursionLevel });
        Console.WriteLine(number);
    }

    private static void ProcessStackSpool()
    {
        //recursion base case
        if (StackSpool.Count == 0)
            return;

        var row = StackSpool.Pop();

        int thisLevel = row.RecursionLevel + 1;
        long thisN = row.N * row.N;

        Debug.Assert(thisLevel <= 100, "max recursion level exceeded");

        if (thisN < 10000000)
            AddToStackSpoolAndEmit(thisN, thisLevel);

        ProcessStackSpool();
    }
}

NB1:如上所述,到锚成员的第一个孩子3被处理时,有关其兄弟姐妹的所有信息,5以及7,及其后代,都已从线轴中丢弃,并且不再可访问。

NB2:上面的C#的总体语义与执行计划相同,但是执行计划中的流程并不相同,因为操作员在那里以流水线执行方式工作。这是一个简化的示例,用于说明该方法的要旨。有关计划本身的更多详细信息,请参见前面的链接。

NB3:堆栈假脱机本身显然是作为非唯一的聚集索引实现的,具有递归级别的关键列,并根据需要添加了唯一标识符(


6
在解析期间,SQL Server中的递归查询始终从递归转换为迭代(使用堆栈)。迭代的实现规则是IterateToDepthFirst- Iterate(seed,rcsv)->PhysIterate(seed,rcsv)。仅供参考。极好的答案。
保罗·怀特说GoFundMonica

顺便说一句,也允许使用UNION代替UNION ALL,但是SQL Server不会这样做。
约书亚

5

这只是(半)有根据的猜测,可能完全错误。顺便问一下有趣的问题。

T-SQL是一种声明性语言。可能将递归CTE转换为游标样式的操作,其中将UNION ALL左侧的结果附加到临时表中,然后UNION ALL的右侧应用于左侧的值。

因此,首先我们将UNION ALL左侧的输出插入结果集中,然后将UNION ALL右侧的结果插入左侧,然后将其插入结果集中。然后将左侧替换为右侧的输出,并将右侧再次应用于“新”左侧。像这样:

  1. {3,5,7}->结果集
  2. 递归语句应用于{3,5,7},即{9,25,49}。{9,25,49}添加到结果集中,并替换UNION ALL的左侧。
  3. 递归语句应用于{9,25,49},即{81,625,2401}。{81,625,2401}已添加到结果集中,并替换了UNION ALL的左侧。
  4. 递归语句应用于{81,625,2401},即{6561,390625,5764801}。{6561,390625,5764801}已添加到结果集。
  5. 游标已完成,因为下一次迭代导致WHERE子句返回false。

您可以在递归CTE的执行计划中看到以下行为:

在此处输入图片说明

这是上面的步骤1,其中UNION ALL的左侧已添加到输出中:

在此处输入图片说明

这是UNION ALL的右侧,其中输出被连接到结果集:

在此处输入图片说明


4

提到T iT i + 1SQL Server文档既不是很容易理解,也不是实际实现的准确描述。

基本思想是查询的递归部分查看所有先前的结果,但查看一次

查看其他数据库如何实现此目的(以获得相同的结果)可能会有所帮助。在Postgres的文件说:

递归查询评估

  1. 评估非递归项。对于UNION(但不是UNION ALL),丢弃重复的行。将所有剩余的行包括在递归查询的结果中,并将它们放置在临时工作表中
  2. 只要工作表不为空,请重复以下步骤:
    1. 评估递归项,将工作表的当前内容替换为递归自引用。对于UNION(但不是UNION ALL),则丢弃重复的行和重复任何先前结果行的行。将所有剩余的行包括在递归查询的结果中,并将它们放在临时中间表中
    2. 用中间表的内容替换工作表的内容,然后清空中间表。

注意
严格来说,此过程不是迭代,而是递归,而是RECURSIVESQL标准委员会选择的术语。

SQLite的文档暗示了一个稍微不同的实现,而这一次,一行地一个时间的算法可能是最容易理解的:

计算递归表内容的基本算法如下:

  1. 运行initial-select并将结果添加到队列中。
  2. 当队列不为空时:
    1. 从队列中提取一行。
    2. 将那一行插入递归表
    3. 假设刚提取的单行是递归表中的唯一行,然后运行recursive-select,将所有结果添加到队列中。

上面的基本过程可以通过以下附加规则进行修改:

  • 如果UNION操作者将initial-selectrecursive-select,则只有在没有相同的行之前已添加到队列行添加到队列中。即使重复行已经从队列中提取出重复的行,重复行也将被丢弃,然后再添加到队列中。如果运算符为UNION ALL,则由initial-select和生成的所有行recursive-select即使重复,也总是添加到队列中。
    […]

0

我的知识专门在DB2中,但是查看说明图似乎与SQL Server相同。

该计划来自这里:

在粘贴计划中查看

SQL Server解释计划

优化器实际上不会为每个递归查询运行全部并集。它采用查询的结构并将联合的第一部分全部分配给“锚成员”,然后它将遍历联合的第二部分(即递归成员),直到达到定义的限制为止。递归完成后,优化器将所有记录连接在一起。

优化器只是建议您执行预定义的操作。

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.