【问题标题】:Recursive Queries CTE递归查询 CTE
【发布时间】:2016-09-06 20:26:03
【问题描述】:

我正坐在“Murach 为开发人员编写的 SQL Server 2016”一书中的示例。该示例说明了如何在 SQL 中编写递归 CTS。我非常了解递归函数(在 C# 中),但我无法理解 sql 递归逻辑是如何工作的。 示例如下:

USE Examples;

WITH EmployeesCTE AS
(
        -- Anchor member
        SELECT EmployeeID, 
            FirstName + ' ' + LastName As EmployeeName, 
            1 As Rank
        FROM Employees
        WHERE ManagerID IS NULL
    UNION ALL
        -- Recursive member
        SELECT Employees.EmployeeID, 
            FirstName + ' ' + LastName, 
            Rank + 1
        FROM Employees
            JOIN EmployeesCTE
            ON Employees.ManagerID = EmployeesCTE.EmployeeID
)
SELECT *
FROM EmployeesCTE
ORDER BY Rank, EmployeeID;

此查询返回组织中员工的层级。

我的问题:在递归函数中,您会看到一个递减变量终止递归(通过达到基本情况)。我的问题是:EmployeesCTE 中的对应部分在哪里?请帮我理解逻辑。

【问题讨论】:

  • 这里INNER JOIN 用作过滤器,它的ON 条件服务器用作“终结者”,只要基础表没有任何循环引用。
  • 根据经验,我想提请您注意这样一个事实,即这在大数据上表现不佳。或者,考虑将 RootManagerID 添加到员工表中。然后SELECT EmployeeID, ManagerID, FirstName + ' ' + LastName As EmployeeName FROM Employees WHERE RootManagerID = @RootManagerID。然后在应用程序级别构建您的树。除了通过这种方法获得巨大的性能优势外,它还允许您通过在 RootManagerID 上创建索引来进一步提高性能。
  • @yazanpro,这将在大数据上表现不佳,因为它是一个隐藏的RBAR...
  • @Shnugo 没错。
  • 嗨,阿里夫,这个问题得到解答了吗?您需要进一步的帮助吗?

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


【解决方案1】:

所以我们所说的“递归 CTE”实际上应该称为迭代 CTE。这个想法是,为了定义一个递归表(在这种情况下为EmployeesCTE),我们首先创建一些初始行,在这种情况下,这是由

   SELECT EmployeeID, 
        FirstName + ' ' + LastName As EmployeeName, 
        1 As Rank
    FROM Employees
    WHERE ManagerID IS NULL

(注意这里不包含对EmployeesCTE的引用,所以它不是递归的),然后我们迭代一个表达式,在这种情况下

    SELECT Employees.EmployeeID, 
        FirstName + ' ' + LastName, 
        Rank + 1
    FROM Employees
        JOIN EmployeesCTE
        ON Employees.ManagerID = EmployeesCTE.EmployeeID

生成更多行。我们这样做,直到该表达式不返回任何行。在这个表达式中,EmployeesCTE 指的是该表的前一个版本,通过评估它,我们计算出该表的下一个版本。

所以停止递归(或者说是迭代)的条件是递归表达式没有产生新行。

现在让我们仔细看看以上所有内容如何应用于您给出的特定示例。我们的初始行集由没有经理的员工组成(我们称他们为 1 级员工)。然后我们找到由上一步找到的员工管理的所有员工(我们称他们为 2 级员工)。然后我们发现员工由 2 级员工管理,称为 3 级,依此类推。最终我们将到达一个找不到新员工的步骤(当然假设被管理的关系没有周期)。

【讨论】:

    【解决方案2】:

    如果您熟悉 C#,您可能会将其视为一个复杂的对象模型

    想象一个简单的 Windows.Forms.Form 及其控件。每个控件本身都有一个控件集合。在数据库中,您可以考虑一个自引用表,其中每一行都指向其父行(顶部对象指向 NULL),就像您的员工指向层次结构中的下一个老板一样。

    有一个带有方法Refresh() 的*对象。当您调用它时,该函数会对自己的内容执行一些操作并在其内部集合上调用Refresh()。该集合在其所有成员上调用Refresh()。他们都做一些事情并在他们的内部收藏上致电Refresh()。这会沿着嵌套模型运行,直到您到达具有空 Controly 集合的控件。

    这更像是一个自上而下的级联。实际上,使用条件故意停止递归 CTE 可能非常棘手,因为您不会获得带有中断条件的最后一行。

    递归 CTE 的第二部分自然结束,此时 JOIN 操作没有返回任何行...

    在您的情况下,您可以将其解读为

    • anchor:获取所有没有老板的员工(*别)
    • 现在询问以其中一位为经理(二级)的所有员工的名单
    • 逐行获取所有以二级人员为经理的员工
    • 继续直到没有更多依赖的员工

    请注意,递归 CTE 在设计上是一种缓慢的方法,因为它是一个隐藏的 RBAR

    【讨论】:

      【解决方案3】:

      可能需要执行 TOP-DOWN MS SQL 以将 TOP 级别值向下传播到所有级别(在某些情况下,只有 top 更好;-)

          SELECT  ID      AS ID
              ,   parent  AS parent
              ,   limit   AS limit
              ,   NULL    AS maxParentLimit -- place holder
              INTO #StructLimit_CTE
              FROM StructLimit_CTE
          CREATE INDEX #StructLimit_CTE_id ON #StructLimit_CTE(ID)
          --SELECT COUNT(*) FROM #StructLimit_CTE -- 1000000+ in my case, so MS needs index  :-)
      
          ;WITH maxParentLimit (ID, ParentLimit, limit) as 
          ( select ID 
                  , limit 
                  , limit             
                  from #StructLimit_CTE
                  where parent IS NULL -- = 0
              union all
                  select o.ID                                     
                      , IIF( o.limit > ParentLimit, o.limit, ParentLimit) -- take the biggst, when we have one, think about NULL-s
                      , o.limit   
                  from #StructLimit_CTE o
                  join maxParentLimit n on n.ID = o.parent -- recursion
          )
          UPDATE #StructLimit_CTE SET maxParentLimit = m.ParentLimit 
              FROM #StructLimit_CTE   AS o    
              JOIN maxParentLimit AS m    ON o.ID = m.ID
          -- or use (LEFT) JOIN when proper
      

      【讨论】: