递归CTE概念混淆

Recursive CTE Concept Confusion

我想了解在我的 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

锚查询会抢到经理。在那之后,我不明白如果递归查询一次又一次地调用锚查询并且锚查询只有一个记录是经理,它将如何带来其他员工。

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

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

这提供了第一组记录。

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

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

第三步将根据第三个结果集等

所以你想了解递归 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 将 return 通过所有这些迭代找到的所有记录。

好吧,递归 CTE 的简短介绍:

递归 CTE 与其说是真正的递归,不如说是迭代。锚查询用于获取一些初始结果集。有了这个集合,我们可以深入研究。试试这些简单的案例:

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

锚点 1 将导致 UNION ALL 中的 2。这个 2 再次被传递到 UNION ALL 并且将被 returned 为 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;

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

运行 单独,初始查询将 return 两行。计数器==1 和 Nmbr-column

的两个不同值
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

结论

重点

  • 锚查询必须 return 至少一行,但可以 return 多行
  • 第二部分必须匹配列列表(如任何 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