递归CTE概念混乱

时间:2019-05-02 07:26:15

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

我试图理解在我的SQL代码中使用CTE的概念。我已经在许多在线文章中解释了这个概念,但是我无法掌握如何迭代来呈现层次结构数据。解释R-CTE的广泛使用的示例之一是Employee and 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

锚查询将获取经理。之后,如果递归查询一次又一次地调用锚查询,并且锚查询只有一个记录即经理,那么我将无法理解其他员工。

3 个答案:

答案 0 :(得分:3)

因此,您想了解递归CTE。

真的很简单。

首先是种子查询,它获取原始记录。
在您的情况下,就是没有经理的员工。
哪个是老板

以一个简化的示例进行演示:

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

第二个查询将查找具有上一个记录作为经理的员工。

由于它是递归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

对于第三次迭代,它将使用第二次迭代中的记录。

...

这一直持续到没有更多的员工加入ManagerID

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

答案 1 :(得分:1)

嗯,递归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:

现在与以前几乎相同,但是有几个元素具有相同的“上一个”。

从原则上讲,这是您的等级制度

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

Conclusio

要点

  • 锚点查询必须至少返回一行,但可能返回很多行
  • 第二部分必须与列列表匹配(与任何UNION ALL查询一样)
  • 第二部分必须在其FROM子句中引用cte
    • 直接或
    • 通过加入
  • 第二部分将使用之前的调用结果反复调用
  • 每行都是单独处理的(隐藏的RBAR
  • 您可以从经理(最上层节点)开始,然后通过查询具有此经理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

答案 2 :(得分:0)

这与递归步骤有关:首先,使用root进行递归的第一步,因此:

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

这提供了第一组记录。

第二组记录将基于第一组记录(锚)进行查询,因此它将查询所有拥有第一组经理的员工。

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

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