【问题标题】:Recursive CTE Concept Confusion递归 CTE 概念混淆
【发布时间】:2019-05-02 07:26:15
【问题描述】:

我正在尝试了解在我的 SQL 代码中使用 CTE 的概念。我浏览了许多在线帖子来解释这个概念,但我无法理解它是如何迭代来呈现分层数据的。解释 R-CTE 的广泛使用的示例之一是 Employee 和 ManagerID 示例,如下所示:

USE AdventureWorks
GO
WITH Emp_CTE AS (
  SELECT EmployeeID, ContactID, LoginID, ManagerID, Title, BirthDate
  FROM HumanResources.Employee
  WHERE ManagerID IS NULL

  UNION ALL

  SELECT e.EmployeeID, e.ContactID, e.LoginID, e.ManagerID, e.Title, e.BirthDate
  FROM HumanResources.Employee e
  INNER JOIN Emp_CTE ecte ON ecte.EmployeeID = e.ManagerID
)
SELECT *
FROM Emp_CTE
GO

锚查询将抓取经理。在那之后,如果递归查询一次又一次地调用锚查询并且锚查询只有一条记录,即经理,我无法理解它将如何带来其他员工。

【问题讨论】:

  • 您的加入是基于 ecte.EmployeeID = e.ManagerID.. 来自锚点的经理的员工 ID 和表中所有员工的经理 ID ..这就是为什么它带来了多个记录

标签: sql sql-server tsql common-table-expression recursive-query


【解决方案1】:

所以您想了解递归 CTE。

真的很简单。

首先是获取原始记录的种子查询。

  SELECT EmployeeID, ContactID, LoginID, ManagerID, Title, BirthDate
  FROM HumanResources.Employee
  WHERE ManagerID IS NULL

在您的情况下,是没有经理的员工。
哪个是老板

用一个简化的例子来演示:

EmployeeID LoginID ManagerID Title 
---------- ------- --------- ------------
101        boss    NULL      The Boss

第二个查询查找之前作为经理的记录的员工。

  SELECT e.EmployeeID, e.ContactID, e.LoginID, e.ManagerID, e.Title, e.BirthDate
  FROM HumanResources.Employee e
  INNER JOIN Emp_CTE ecte ON ecte.EmployeeID = e.ManagerID

由于它是递归 CTE,因此 CTE 在第二个查询中使用自身。
您可以将其视为一个循环,它使用以前的记录来获取下一个记录。

对于该递归循环的第一次迭代,您可以得到如下内容:

 EmployeeID LoginID ManagerID Title 
---------- ------- --------- ------------
102        head1    101      Top Manager 1
103        head2    101      Top Manager 2

对于第二次迭代,它将使用第一次迭代的记录来查找下一次迭代。

 EmployeeID LoginID ManagerID Title 
---------- ------- --------- ------------

104        bob     102       Department Manager 1
105        hilda   102       Department Manager 2

108        john    103       Department Manager 4
109        jane    103       Department Manager 5

对于第 3 次迭代,它将使用来自第 2 次迭代的记录。

...

这种情况一直持续到 ManagerID 上没有更多员工加入为止

然后在所有循环之后,CTE 将返回通过所有这些迭代找到的所有记录。

【讨论】:

  • 非常感谢 LukStorms,我理解了这个概念,现在它很有意义。但是你能提出一些问题,我可以使用 R-CTE 来进一步巩固这个概念并做一些练习。
  • @Zee786 谢谢。好吧,我想你可以寻找关于寻找递归解决方案的问题。 F.e. searching for them?但递归 CTE 并不总是最好的选择。一些问题,也可以通过递归解决,也可以通过window functions 更有效地解决,例如 ROW_NUMBER 或 LAG/LEAD。
【解决方案2】:

嗯,递归 CTE 的简短介绍:

递归 CTE 是一种迭代,而不是真正的递归。锚查询被用来获取一些初始结果集。有了这套,我们可以更深入地潜水。试试这些简单的案例:

只是一个计数器,甚至不需要 JOIN...

锚点的 1 将导致 UNION ALL 中的 2。这个 2 再次传递到 UNION ALL 中,并将作为 3 返回,依此类推...

WITH recCTE AS
(
    SELECT 1 AS Mycounter 

    UNION ALL

    SELECT recCTE.MyCounter+1
    FROM recCTE 
    WHERE recCTE.MyCounter<10
)
SELECT * FROM recCTE;

2 列的计数器

这和上面的完全一样。但是我们有两列,分别处理。

WITH recCTE AS
(
    SELECT 1 AS Mycounter1, 10 AS MyCounter2 

    UNION ALL

    SELECT recCTE.MyCounter1+1,recCTE.MyCounter2+1
    FROM recCTE 
    WHERE recCTE.MyCounter1<10
)
SELECT * FROM recCTE;

现在我们在初始查询中有两行

单独运行,初始查询将返回两行。两者都有 counter==1 和 Nmbr 列的两个不同值

WITH recCTE AS
(
    SELECT MyCounter=1, Nmbr FROM(VALUES(1),(10)) A(Nmbr)

    UNION ALL

    SELECT recCTE.MyCounter+1, recCTE.Nmbr+1
    FROM recCTE 
    WHERE recCTE.MyCounter<10
)
SELECT * FROM recCTE ORDER BY MyCounter,Nmbr;

现在我们得到了 20 行,而不是之前示例中的 10 行。这是因为锚的两行都是独立使用的。

我们可以在 JOIN 中使用递归 CTE

在本例中,我们将首先创建一个派生集,然后将其连接到递归 CTE。猜猜为什么第一行带有“X”而不是“A”?

WITH SomeSet AS (SELECT * FROM (VALUES(1,'A'),(2,'B'),(3,'C'),(4,'D'),(5,'E'),(6,'F'),(7,'G'),(8,'H'),(9,'I'),(10,'J')) A(id,Letter))
,recCTE AS
(
    SELECT MyCounter=1, Nmbr,'X' AS Letter FROM(VALUES(1),(10)) A(Nmbr)

    UNION ALL

    SELECT recCTE.MyCounter+1, recCTE.Nmbr+1, SomeSet.Letter
    FROM SomeSet 
    INNER JOIN recCTE ON SomeSet.id=recCTE.MyCounter+1
    WHERE recCTE.MyCounter<10
)
SELECT * FROM recCTE ORDER BY MyCounter,Nmbr;

这将使用自引用连接来模拟您的层次结构,但使用一个无间隙链

WITH SomeSet AS (SELECT * FROM (VALUES(1,'A',NULL),(2,'B',1),(3,'C',2),(4,'D',3),(5,'E',4),(6,'F',5),(7,'G',6),(8,'H',7),(9,'I',8),(10,'J',9)) A(id,Letter,Previous))
,recCTE AS
(
    SELECT id,Letter,Previous,' ' PreviousLetter FROM SomeSet WHERE Previous IS NULL

    UNION ALL

    SELECT SomeSet.id,SomeSet.Letter,SomeSet.Previous,recCTE.Letter
    FROM SomeSet 
    INNER JOIN recCTE ON SomeSet.Previous=recCTE.id
)
SELECT * FROM recCTE:

现在几乎和以前一样了,但有几个元素具有相同的“previous”。

这是 - 原则上 - 你的层次结构

WITH SomeSet AS (SELECT * FROM (VALUES(1,'A',NULL),(2,'B',1),(3,'C',2),(4,'D',2),(5,'E',2),(6,'F',3),(7,'G',3),(8,'H',4),(9,'I',1),(10,'J',9)) A(id,Letter,Previous))
,recCTE AS
(
    SELECT id,Letter,Previous,' ' PreviousLetter FROM SomeSet WHERE Previous IS NULL

    UNION ALL

    SELECT SomeSet.id,SomeSet.Letter,SomeSet.Previous,recCTE.Letter
    FROM SomeSet 
    INNER JOIN recCTE ON SomeSet.Previous=recCTE.id
)
SELECT * FROM recCTE

结论

重点

  • 锚查询必须返回至少一行,但可能返回多行
  • 第二部分必须与列列表匹配(与任何UNION ALL 查询一样)
  • 第二部分必须在其FROM子句中引用cte
    • 直接或
    • 通过 JOIN
  • 第二部分将使用之前调用的结果反复调用
  • 每一行单独处理(一个隐藏的RBAR
  • 您可以从经理(top-most-node)开始,然后通过查询具有此经理 ID 的员工来向下走,或者
  • 您可以从层次结构中最低的开始(没有其他行的那些,使用它们的 id 作为经理 id)并向上移动列表
  • 由于它是一个隐藏的 RBAR,您可以将它用于 逐行 操作,例如字符串累积。

最后一条语句的示例

查看 LetterPath 列是如何构建的。

WITH SomeSet AS (SELECT * FROM (VALUES(1,'A',NULL),(2,'B',1),(3,'C',2),(4,'D',2),(5,'E',2),(6,'F',3),(7,'G',3),(8,'H',4),(9,'I',1),(10,'J',9)) A(id,Letter,Previous))
,recCTE AS
(
    SELECT id,Letter,Previous,' ' PreviousLetter,CAST(Letter AS VARCHAR(MAX)) AS LetterPath FROM SomeSet WHERE Previous IS NULL

    UNION ALL

    SELECT SomeSet.id,SomeSet.Letter,SomeSet.Previous,recCTE.Letter,recCTE.LetterPath + SomeSet.Letter 
    FROM SomeSet 
    INNER JOIN recCTE ON SomeSet.Previous=recCTE.id
)
SELECT * FROM recCTE

【讨论】:

    【解决方案3】:

    这都是关于递归步骤的:首先,root用于进行递归的第一步,所以:

    SELECT EmployeeID, ContactID, LoginID, ManagerID, Title, BirthDate
    FROM HumanResources.Employee
    WHERE ManagerID IS NULL
    

    这提供了第一组记录。

    第二组记录将根据第一组(锚点)查询,因此它将查询所有员工,其中经理在第一组。

    递归的第二步将基于第二个结果集,不是锚点

    第三步将基于第三个结果集等。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-02-12
      • 1970-01-01
      • 2016-05-08
      • 1970-01-01
      • 2015-04-27
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多