【问题标题】:SQL Server: How to limit CTE recursion to rows just recursivly added?SQL Server:如何将 CTE 递归限制为仅递归添加的行?
【发布时间】:2015-12-05 06:12:45
【问题描述】:

更简单的例子

让我们尝试一个更简单的示例,这样人们就可以围绕这些概念展开头脑风暴,并提供一个可以复制并粘贴到 SQL Query Analizer 中的实际示例:

想象一个 Nodes 表,有一个层次结构:

A
 - B
    - C

我们可以在查询分析器中开始测试:

CREATE TABLE ##Nodes
(
 NodeID varchar(50) PRIMARY KEY NOT NULL,
 ParentNodeID varchar(50) NULL
)

INSERT INTO ##Nodes (NodeID, ParentNodeID) VALUES ('A', null)
INSERT INTO ##Nodes (NodeID, ParentNodeID) VALUES ('B', 'A')
INSERT INTO ##Nodes (NodeID, ParentNodeID) VALUES ('C', 'B')

期望的输出:

ParentNodeID    NodeID    GenerationsRemoved
============    ======    ==================
NULL            A         1
NULL            B         2
NULL            C         3
A               B         1
A               C         2
B               C         1

现在建议的 CTE 表达式,输出不正确:

WITH NodeChildren AS
(
   --initialization
   SELECT ParentNodeID, NodeID, 1 AS GenerationsRemoved
   FROM ##Nodes
   WHERE ParentNodeID IS NULL

   UNION ALL

   --recursive execution
   SELECT P.ParentNodeID, N.NodeID, P.GenerationsRemoved + 1
   FROM NodeChildren AS P
      INNER JOIN ##Nodes AS N
      ON P.NodeID = N.ParentNodeID
)
SELECT ParentNodeID, NodeID, GenerationsRemoved
FROM NodeChildren

实际输出

ParentNodeID    NodeID    GenerationsRemoved
============    ======    ==================
NULL            A         1
NULL            B         2
NULL            C         3

注意:如果 SQL Server 2005† CTE 无法完成我在 2000 年之前所做的事情‡,那很好,这就是答案。给出“不可能”作为答案的人将赢得赏金。但我会等几天,以确保每个人都同意这是不可能的,然后我不可挽回地给予 250 声望,因为我没有解决我的问题。

吹毛求疵者角

†不是 2008 年

‡无需求助于 UDF*,这是已有的解决方案

*除非你能在原始问题中看到提高 UDF 性能的方法


原始问题

我有一个节点表,每个节点都有一个指向另一个节点(或 null)的父节点。

为了说明:

1 My Computer
    2 Drive C
         4 Users
         5 Program Files
         7 Windows
             8 System32
    3 Drive D
         6 mp3

我想要一个返回所有父子关系的表,以及它们之间的世代数

对于所有直接的父关系:

ParentNodeID  ChildNodeID  GenerationsRemoved
============  ===========  ===================
(null)        1            1
1             2            1
2             4            1
2             5            1
2             7            1
1             3            1
3             6            1
7             8            1

但是还有祖父母关系:

ParentNodeID  ChildNodeID  GenerationsRemoved
============  ===========  ===================
(null)        2            2
(null)        3            2
1             4            2
1             5            2
1             7            2
1             6            2
2             8            2

还有曾祖父母关系:

ParentNodeID  ChildNodeID  GenerationsRemoved
============  ===========  ===================
(null)        4            3
(null)        5            3
(null)        7            3
(null)        6            3
1             8            3

所以我可以弄清楚基本的 CTE 初始化:

WITH (NodeChildren) AS
{
   --initialization
   SELECT ParentNodeID, NodeID AS ChildNodeID, 1 AS GenerationsRemoved
   FROM Nodes
} 

现在的问题是递归部分。当然,显而易见的答案是行不通的:

WITH (NodeChildren) AS
{
   --initialization
   SELECT ParentNodeID, ChildNodeID, 1 AS GenerationsRemoved
   FROM Nodes

   UNION ALL

   --recursive execution
   SELECT parents.ParentNodeID, children.NodeID, parents.Generations+1
   FROM NodeChildren parents
    INNER JOIN NodeParents children
    ON parents.NodeID = children.ParentNodeID
} 

Msg 253, Level 16, State 1, Line 1
Recursive member of a common table expression 'NodeChildren' has multiple recursive references.

生成整个递归列表所需的所有信息都存在于初始 CTE 表中。但如果不允许这样做,我会尝试:

WITH (NodeChildren) AS
{
   --initialization
   SELECT ParentNodeID, NodeID, 1 AS GenerationsRemoved
   FROM Nodes

   UNION ALL

   --recursive execution
   SELECT parents.ParentNodeID, Nodes.NodeID, parents.Generations+1
   FROM NodeChildren parents
    INNER JOIN Nodes
    ON parents.NodeID = nodes.ParentNodeID
} 

但这失败了,因为它不仅加入递归元素,而且一遍又一遍地递归添加相同的行:

Msg 530, Level 16, State 1, Line 1
The statement terminated. The maximum recursion 100 has been exhausted before statement completion.

在 SQL Server 2000 中,我使用用户定义函数 (UDF) 模拟了 CTE:

CREATE FUNCTION [dbo].[fn_NodeChildren] ()
RETURNS @Result TABLE (
    ParentNodeID int NULL,
    ChildNodeID int NULL,
    Generations int NOT NULL) 
AS  
/*This UDF returns all "ParentNode" - "Child Node" combinations
    ...even multiple levels separated
BEGIN 
    DECLARE @Generations int
    SET @Generations = 1

    --Insert into the Return table all "Self" entries
    INSERT INTO @Result
    SELECT ParentNodeID, NodeID, @Generations
    FROM Nodes
    WHILE @@rowcount > 0 
    BEGIN
        SET @Generations = @Generations + 1
        --Add to the Children table: 
        --  children of all nodes just added 
        -- (i.e. Where @Result.Generation = CurrentGeneration-1)
        INSERT @Result
        SELECT CurrentParents.ParentNodeID, Nodes.NodeID, @Generations
        FROM Nodes
            INNER JOIN @Result CurrentParents
            ON Nodes.ParentNodeID = CurrentParents.ChildNodeID
        WHERE CurrentParents.Generations = @Generations - 1
    END
    RETURN
END

阻止它爆炸的魔法是限制 where 子句: WHERE CurrentParents.Generations - @Generations-1

如何防止递归 CTE 永远递归?

【问题讨论】:

  • 老实说 - 我认为您无法通过简单的一步完成此操作。您要实现的不是从级别到级别的“正常”分层显示,而是多个级别之间的混合。这是CTE或其他方法无法处理的......

标签: sql-server common-table-expression


【解决方案1】:

试试这个:

WITH Nodes AS
(
   --initialization
   SELECT ParentNodeID, NodeID, 1 AS GenerationsRemoved
   FROM ##Nodes

   UNION ALL

   ----recursive execution
   SELECT P.ParentNodeID, N.NodeID, P.GenerationsRemoved + 1
   FROM Nodes AS P
      INNER JOIN ##Nodes AS N
      ON P.NodeID = N.ParentNodeID
   WHERE P.GenerationsRemoved <= 10

)
SELECT ParentNodeID, NodeID, GenerationsRemoved
FROM Nodes
ORDER BY ParentNodeID, NodeID, GenerationsRemoved

基本上从初始化查询中删除“只显示绝对父母”;这样,它会生成从它们中的每一个开始并从那里下降的结果。我还在“WHERE P.GenerationsRemoved

【讨论】:

  • 绝妙的解决方案!如果你仍然得到最大递归错误,那是因为你的层次结构中有循环引用。
  • 您可以正确地使用 OPTION (MAXRECURSION nn) 限制递归。这不是正确的解决方案
  • OPTION(MAXRECURSION nn) 导致语句失败;我包含 WHERE 子句的目标是保证返回(显然有时失败会更好)。
  • 此解决方案适用于 MAXRECURSION 不适用的我。我有一个遗留数据的特殊情况,递归太远通常会找到自引用数据。这使我只能下降 n 深,如果您将 MAXRECURSION 设置为 5,它将在 5 上抛出错误。此解决方案返回前 5,无论是否存在无限循环。
【解决方案2】:

另外:你有 SQL Server 2008 吗?这可能适合hierarchyid data type

【讨论】:

  • 不,没有2008。而在2003年我本来需要解决问题的时候,人们说要等2005年。
  • 另外,我无法获得 2008,安装它,将数据库移植到 2008,重新设计整个系统,让客户购买、安装和配置 2008 - 所有这些都在接下来的几个小时内完成.
【解决方案3】:

如果我理解您的意图,您可以通过执行以下操作来获得结果:

DECLARE @StartID INT;
SET @StartID = 1;
WITH CTE (ChildNodeID, ParentNodeID, [Level]) AS
(
  SELECT  t1.ChildNodeID, 
          t1.ParentNodeID, 
          0
  FROM tblNodes AS t1
  WHERE ChildNodeID = @StartID
  UNION ALL
  SELECT  t1.ChildNodeID, 
          t1.ParentNodeID, 
          t2.[Level]+1
  FROM tblNodes AS t1
    INNER JOIN CTE AS t2 ON t1.ParentNodeID = t2.ChildNodeID    
)
SELECT t1.ChildNodeID, t2.ChildNodeID, t1.[Level]- t2.[Level] AS GenerationsDiff
FROM CTE AS t1
  CROSS APPLY CTE t2

这将返回所有节点之间的代差,您可以根据需要进行修改。

【讨论】:

  • 这不起作用,因为它只从特定节点开始 - 我需要所有节点。然后是所有这些节点的子节点。然后是所有这些节点的孩子。等等
【解决方案4】:

嗯,你的答案并不那么明显:-)

WITH (NodeChildren) AS
{
   --initialization
   SELECT ParentNodeID, ChildNodeID, 1 AS GenerationsRemoved
   FROM Nodes

这部分被称为递归 CTE 的“锚”部分 - 但它实际上应该只从您的表中选择一个或几行 - 这会选择所有内容!

我猜你在这里缺少的只是一个合适的 WHERE 子句:

WITH (NodeChildren) AS
{
   --initialization
   SELECT ParentNodeID, ChildNodeID, 1 AS GenerationsRemoved
   FROM Nodes
   **WHERE ParentNodeID IS NULL**

但是,恐怕您不仅要具有“直”层次结构,而且还要具有祖父子行的要求,可能并不容易满足....通常递归 CTE 只会显示一个级别及其直接下属(当然还有下级)——通常不会跳过一个、两个甚至更多级别。

希望这会有所帮助。

马克

【讨论】:

  • 我不能将它限制在没有父级的行,因为它会限制到没有父级的行。我还需要包含父行。
【解决方案5】:

问题在于 Sql Server 默认递归限制 (100)。如果您在顶部尝试您的示例,并删除了锚限制(也添加了 Order By):

WITH NodeChildren AS
(
   --initialization
   SELECT ParentNodeID, NodeID, 1 AS GenerationsRemoved
   FROM Nodes

   UNION ALL

   --recursive execution
   SELECT P.ParentNodeID, N.NodeID, P.GenerationsRemoved + 1
   FROM NodeChildren AS P
      inner JOIN Nodes AS N
      ON P.NodeID = N.ParentNodeID
)
SELECT ParentNodeID, NodeID, GenerationsRemoved
FROM NodeChildren
ORDER BY ParentNodeID ASC

这会产生所需的结果。您面临的问题是您将重复超过 100 次的大量行,这是默认限制。这可以通过在查询后添加option (max recursion x) 来更改,其中 x 是介于 1 和 32767 之间的数字。x 也可以设置为 0,这没有限制,但很快会对您的服务器性能产生非常不利的影响。显然,随着 Nodes 中行数的增加,递归数会迅速增加,除非表中的行数已知上限,否则我会避免这种方法。为了完整起见,最终查询应如下所示:

 WITH NodeChildren AS
    (
       --initialization
       SELECT ParentNodeID, NodeID, 1 AS GenerationsRemoved
       FROM Nodes

       UNION ALL

       --recursive execution
       SELECT P.ParentNodeID, N.NodeID, P.GenerationsRemoved + 1
       FROM NodeChildren AS P
          inner JOIN Nodes AS N
          ON P.NodeID = N.ParentNodeID
    )
    SELECT * 
    FROM NodeChildren
    ORDER BY ParentNodeID
    OPTION (MAXRECURSION 32767)

32767 可以向下调整以适应您的场景

【讨论】:

    【解决方案6】:

    您是否尝试过在 CTE 中构建路径并使用它来识别祖先?

    然后您可以从祖先节点深度中减去后代节点深度来计算 GenerationsRemoved 列,如下所示...

    DECLARE @Nodes TABLE
    (
        NodeId varchar(50) PRIMARY KEY NOT NULL,
        ParentNodeId varchar(50) NULL
    )
    
    INSERT INTO @Nodes (NodeId, ParentNodeId) VALUES ('A', NULL)
    INSERT INTO @Nodes (NodeId, ParentNodeId) VALUES ('B', 'A')
    INSERT INTO @Nodes (NodeId, ParentNodeId) VALUES ('C', 'B')
    
    DECLARE @Hierarchy TABLE
    (
        NodeId varchar(50) PRIMARY KEY NOT NULL,
        ParentNodeId varchar(50) NULL,
        Depth int NOT NULL,
        [Path] varchar(2000) NOT NULL
    )
    
    WITH Hierarchy AS
    (
        --initialization
        SELECT NodeId, ParentNodeId, 0 AS Depth, CONVERT(varchar(2000), NodeId) AS [Path]
        FROM @Nodes
        WHERE ParentNodeId IS NULL
    
        UNION ALL
    
        --recursive execution
        SELECT n.NodeId, n.ParentNodeId, p.Depth + 1, CONVERT(varchar(2000), p.[Path] + '/' + n.NodeId)
        FROM Hierarchy AS p
        INNER JOIN @Nodes AS n
        ON p.NodeId = n.ParentNodeId
    )
    INSERT INTO @Hierarchy
    SELECT *
    FROM Hierarchy
    
    SELECT parent.NodeId AS AncestorNodeId, child.NodeId AS DescendantNodeId, child.Depth - parent.Depth AS GenerationsRemoved
    FROM @Hierarchy AS parent
    INNER JOIN @Hierarchy AS child
    ON child.[Path] LIKE parent.[Path] + '/%'
    

    【讨论】:

      【解决方案7】:

      这打破了强加给 Chris Shaffer 答案的递归限制。

      我用循环创建一个表:

      CREATE TABLE ##Nodes
      (
         NodeID varchar(50) PRIMARY KEY NOT NULL,
         ParentNodeID varchar(50) NULL
      )
      
      INSERT INTO ##Nodes (NodeID, ParentNodeID) VALUES ('A', 'C');
      INSERT INTO ##Nodes (NodeID, ParentNodeID) VALUES ('B', 'A');
      INSERT INTO ##Nodes (NodeID, ParentNodeID) VALUES ('C', 'B');
      

      在存在潜在循环的情况下(即 ParentNodeId IS NOT NULL),删除的生成从 2 开始。然后我们可以通过检查 (P.ParentNodeID == N.NodeID) 来识别循环,我们只是不这样做'不要添加它。之后,我们追加省略的生成 remove = 1。

      WITH ParentNodes AS
      (
         --initialization
         SELECT ParentNodeID, NodeID, 1 AS GenerationsRemoved
         FROM ##Nodes
         WHERE ParentNodeID IS NULL
      
         UNION ALL
      
         SELECT P.ParentNodeID, N.NodeID, 2 AS GenerationsRemoved
         FROM ##Nodes N
         JOIN ##Nodes P ON N.ParentNodeID=P.NodeID
         WHERE P.ParentNodeID IS NOT NULL
      
         UNION ALL
      
         ----recursive execution
         SELECT P.ParentNodeID, N.NodeID, P.GenerationsRemoved + 1
         FROM ParentNodes AS P
           INNER JOIN ##Nodes AS N
           ON P.NodeID = N.ParentNodeID
         WHERE P.ParentNodeID IS NULL OR P.ParentNodeID <> N.NodeID
      
      ),
      Nodes AS (
         SELECT ParentNodeID, NodeID, 1 AS GenerationsRemoved 
         FROM ##Nodes 
         WHERE ParentNodeID IS NOT NULL
      
         UNION ALL
      
         SELECT ParentNodeID, NodeID, GenerationsRemoved FROM ParentNodes
      )
      SELECT ParentNodeID, NodeID, GenerationsRemoved
      FROM Nodes
      ORDER BY ParentNodeID, NodeID, GenerationsRemoved
      

      【讨论】:

        【解决方案8】:
        with cte as
        (
            select a=65, L=1
            union all
            select a+1, L=L+1
            from cte
            where L<=100
        )
        select 
        IsRecursion=Case When L>1 then 'Recursion' else 'Not Recursion' end,
        AsciiValue=a,
        AsciiCharacter=char(a)
        from cte
        
        1. 创建一个包含当前级别的列。
        2. 检查级别是否>1

        我在这里的示例显示了一个递归 CTE,它在 100 个级别(最大值)后停止递归。作为奖励,它显示了一堆 ASCII 字符和相应的数值。

        【讨论】:

          猜你喜欢
          • 2014-08-04
          • 1970-01-01
          • 2012-04-23
          • 2014-05-19
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-12-25
          相关资源
          最近更新 更多