【问题标题】:SQL Server 2005 recursive query with loops in data - is it possible?带有数据循环的 SQL Server 2005 递归查询 - 可能吗?
【发布时间】:2009-07-28 09:22:57
【问题描述】:

我有一个标准的老板/下属员工表。我需要选择一个老板(由 ID 指定)和他的所有下属(及其下属等)。不幸的是,现实世界的数据中有一些循环(例如,两个公司所有者都将对方设置为他们的老板)。带有 CTE 阻塞的简单递归查询(超过最大递归级别 100)。员工还能选吗?我不在乎它们被选中的顺序,只关心它们中的每一个都被选中一次。


补充:你想要我的查询吗?嗯......好吧......我虽然很明显,但是 - 这里是:
with
UserTbl as -- Selects an employee and his subordinates.
(
    select a.[User_ID], a.[Manager_ID] from [User] a WHERE [User_ID] = @UserID
    union all
    select a.[User_ID], a.[Manager_ID] from [User] a join UserTbl b on (a.[Manager_ID]=b.[User_ID])
)
select * from UserTbl


添加 2: 哦,如果不清楚 - 这是一个生产系统,我必须做一些升级(基本上是添加一种报告)。因此,如果可以避免,我宁愿不修改数据。

【问题讨论】:

  • /你能发布你的查询吗?

标签: sql sql-server recursion loops common-table-expression


【解决方案1】:

我知道这已经有一段时间了,但我认为我应该分享我的经验,因为我尝试了每一个解决方案,这里是我的发现摘要(也许是这篇文章?):

  • 使用当前路径添加列确实有效,但性能受到影响,因此对我来说不是一个选项。
  • 我找不到使用 CTE 的方法。
  • 我编写了一个递归 SQL 函数,它将employeeIds 添加到表中。为了绕过循环引用,需要检查以确保没有重复的 ID 被添加到表中。表现一般,但并不理想。

完成所有这些后,我想出了将 [合格] 员工的整个子集转储到代码 (C#) 并使用递归方法在那里过滤它们的想法。然后我将过滤后的员工列表写入数据表,并将其作为临时表导出到我的存储过程中。令我难以置信的是,对于小型和相对较大的表(我尝试了多达 35,000 行的表),这被证明是最快和最灵活的方法。

【讨论】:

    【解决方案2】:

    这适用于初始递归链接,但可能不适用于更长的链接

    DECLARE @Table TABLE(
            ID INT,
            PARENTID INT
    )
    
    INSERT INTO @Table (ID,PARENTID) SELECT 1, 2
    
    INSERT INTO @Table (ID,PARENTID) SELECT 2, 1
    
    INSERT INTO @Table (ID,PARENTID) SELECT 3, 1
    
    INSERT INTO @Table (ID,PARENTID) SELECT 4, 3
    
    INSERT INTO @Table (ID,PARENTID) SELECT 5, 2
    
    
    SELECT * FROM @Table
    
    DECLARE @ID INT
    
    SELECT @ID = 1
    
    ;WITH boss (ID,PARENTID) AS (
        SELECT  ID,
                PARENTID
        FROM    @Table
        WHERE   PARENTID = @ID
    ),
     bossChild (ID,PARENTID) AS (
        SELECT  ID,
                PARENTID
        FROM    boss
        UNION ALL
        SELECT  t.ID,
                t.PARENTID
        FROM    @Table t INNER JOIN
                bossChild b ON t.PARENTID = b.ID
        WHERE   t.ID NOT IN (SELECT PARENTID FROM boss)
    )
    SELECT  *
    FROM    bossChild
    OPTION (MAXRECURSION 0)
    

    我建议使用 while 循环,并且仅在 id 不存在时将链接插入临时表,从而消除无限循环。

    【讨论】:

    • 好主意,但考虑到您提到的限制,使用 van 的简单答案可能会更好。
    【解决方案3】:

    不是通用解决方案,但可能适用于您的情况:在您的选择查询中修改:

    select a.[User_ID], a.[Manager_ID] from [User] a join UserTbl b on (a.[Manager_ID]=b.[User_ID])
    

    变成:

    select a.[User_ID], a.[Manager_ID] from [User] a join UserTbl b on (a.[Manager_ID]=b.[User_ID]) 
       and a.[User_ID] <> @UserID
    

    【讨论】:

    • 如果员工最多可以有 1 个经理,那么它应该可以正常工作。如果没有,您可能会在较低级别获得循环,这不会停止递归。
    【解决方案4】:

    您不必递归地执行此操作。它可以在 WHILE 循环中完成。我保证它会更快:我每次对这两种技术进行计时都是如此。这听起来效率低下,但不是因为循环数是递归级别。在每次迭代中,您都可以检查循环并纠正它发生的位置。如果发生循环,您还可以对临时表施加约束以触发错误,尽管您似乎更喜欢更优雅地处理循环的东西。您还可以在 while 循环迭代一定数量的级别时触发错误(以捕获未检测到的循环?-天哪,有时会发生。

    诀窍是重复插入临时表(以根条目为起始),包括具有当前迭代号的列,并在临时表中的最新结果和子条目之间进行内部连接在原始表中。当@@rowcount=0 时跳出循环! 简单吧?

    【讨论】:

      【解决方案5】:

      我知道您不久前问过这个问题,但这里有一个可能适用于检测无限递归循环的解决方案。我生成了一个路径,如果 USER ID 在路径中,我检查了 CTE 条件,如果是,则不会再次处理它。希望这会有所帮助。

      何塞

      DECLARE @Table TABLE(
          USER_ID INT,
          MANAGER_ID INT )
      INSERT INTO @Table (USER_ID,MANAGER_ID) SELECT 1, 2
      INSERT INTO @Table (USER_ID,MANAGER_ID) SELECT 2, 1
      INSERT INTO @Table (USER_ID,MANAGER_ID) SELECT 3, 1
      INSERT INTO @Table (USER_ID,MANAGER_ID) SELECT 4, 3
      INSERT INTO @Table (USER_ID,MANAGER_ID) SELECT 5, 2
      
      DECLARE @UserID INT
      SELECT @UserID = 1
      
      ;with
      UserTbl as -- Selects an employee and his subordinates.
      (
          select 
              '/'+cast( a.USER_ID as varchar(max)) as [path],
              a.[User_ID], 
              a.[Manager_ID] 
          from @Table a 
          where [User_ID] = @UserID
          union all
          select
              b.[path] +'/'+ cast( a.USER_ID as varchar(max)) as [path],
              a.[User_ID], 
              a.[Manager_ID] 
          from @Table a 
          inner join UserTbl b 
              on (a.[Manager_ID]=b.[User_ID])
          where charindex('/'+cast( a.USER_ID as varchar(max))+'/',[path]) = 0
      )
      select * from UserTbl
      

      【讨论】:

      • 不错的一个!可能不是很快,但仍然很好! :) 哦,顺便说一句——问这个问题的不是约翰。 :)
      • 嗯...我认为如果重复的用户 ID 位于路径的末尾,这会产生一个微不足道的错误。路径中的最终用户 ID 不包含尾部斜杠,但 charindex() 需要它。
      • 嗨 Vilx,很抱歉给你打电话约翰,但我对约翰所做的编辑感到困惑。您将遇到您所描述的场景的唯一一次是当用户 ID 指向自身时,在这种情况下,它只会向您显示关系一次,而不会进入无限循环。有时错误会变成功能,一切都取决于旁观者的眼光。我使用了这个解决方案,但 id 是唯一标识符,我不需要使用尾部斜杠,但数字是另一回事。问候。 :)
      【解决方案6】:

      基本上,如果您在数据中有这样的循环,则必须自己执行检索逻辑。 您可以使用一个 cte 只获得下属,而使用其他 cte 获得老板。

      另一个想法是让两个公司所有者都担任老板,这样他们就不会成为彼此的老板,这很荒谬。这是我的首选。

      【讨论】:

      • 修复数据是一个选项,但我宁愿把它作为最后一个选项(顺便说一句 - 一个人也可能没有老板,所以我什至不必创建一个虚拟用户)。
      • 另外,我认为还有其他循环。
      • 唯一的办法就是创建自己的逻辑。
      【解决方案7】:

      我可以想到两种方法。

      1) 生成比您想要的更多的行,但包括一个检查以确保它不会递归太深。然后删除重复的用户记录。

      2) 使用字符串来保存已经访问过的用户。就像不起作用的不在子查询中的想法一样。

      方法一:

      ; with TooMuchHierarchy as (
          select "User_ID"
              , Manager_ID 
              , 0 as Depth
          from "User" 
          WHERE "User_ID" = @UserID
          union all
          select U."User_ID"
              , U.Manager_ID
              , M.Depth + 1 as Depth
          from TooMuchHierarchy M
          inner join "User" U 
              on U.Manager_ID = M."user_id"
          where Depth < 100) -- Warning MAGIC NUMBER!!
      , AddMaxDepth as (
          select "User_ID"
              , Manager_id
              , Depth
              , max(depth) over (partition by "User_ID") as MaxDepth
          from TooMuchHierarchy)
      select "user_id", Manager_Id 
      from AddMaxDepth
      where Depth = MaxDepth
      

      where Depth &lt; 100 行是阻止您获得最大递归错误的原因。使这个数字更小,将产生更少的需要丢弃的记录。将其设置得太小,员工将不会被退回,因此请确保它至少与存储的组织结构图的深度一样大。随着公司的发展,这是一场维护噩梦。如果需要更大,则将option (maxrecursion ... number ...) 添加到整个事物中以允许更多递归。

      方法二:

      ; with Hierarchy as (
          select "User_ID"
              , Manager_ID 
              , '#' + cast("user_id" as varchar(max)) + '#' as user_id_list
          from "User" 
          WHERE "User_ID" = @UserID
          union all
          select U."User_ID"
              , U.Manager_ID
              , M.user_id_list + '#' + cast(U."user_id" as varchar(max)) + '#' as user_id_list
          from Hierarchy M
          inner join "User" U 
              on U.Manager_ID = M."user_id"
          where user_id_list not like '%#' + cast(U."User_id" as varchar(max)) + '#%')
      select "user_id", Manager_Id 
      from Hierarchy
      

      【讨论】:

        【解决方案8】:

        首选的解决方案是清理数据并确保您将来没有任何循环 - 这可以通过包含在检查约束中的触发器或 UDF 来完成。

        但是,您可以使用我在这里演示的多语句 UDF:Avoiding infinite loops. Part One

        您可以在连接中添加 NOT IN() 子句以过滤掉循环。

        【讨论】:

        • 是的,这就是我最终解决的方法,但问题仍然有效,对于某些配置,这可能是无论如何都需要的(例如,如果您想选择所有可以从特定起始节点到达的图中的节点)。
        【解决方案9】:

        这是我在一个项目中用来向上和向下追踪层次关系树的代码。

        用户定义的捕获下属的函数:

        CREATE FUNCTION fn_UserSubordinates(@User_ID INT)
        RETURNS @SubordinateUsers TABLE (User_ID INT, Distance INT) AS BEGIN
            IF @User_ID IS NULL
                RETURN
        
            INSERT INTO @SubordinateUsers (User_ID, Distance) VALUES ( @User_ID, 0)
        
            DECLARE @Distance INT, @Finished BIT
            SELECT @Distance = 1, @Finished = 0
        
            WHILE @Finished = 0
            BEGIN
                INSERT INTO @SubordinateUsers
                    SELECT S.User_ID, @Distance
                        FROM Users AS S
                        JOIN @SubordinateUsers AS C
                            ON C.User_ID = S.Manager_ID
                        LEFT JOIN @SubordinateUsers AS C2
                            ON C2.User_ID = S.User_ID
                        WHERE C2.User_ID IS NULL
                IF @@RowCount = 0
                    SET @Finished = 1
        
                SET @Distance = @Distance + 1
            END
        
            RETURN
        END
        

        用于捕获经理的用户定义函数:

        CREATE FUNCTION fn_UserManagers(@User_ID INT)
        RETURNS @User TABLE (User_ID INT, Distance INT) AS BEGIN
            IF @User_ID IS NULL
                RETURN
        
            DECLARE @Manager_ID INT
        
            SELECT @Manager_ID = Manager_ID
            FROM UserClasses WITH (NOLOCK)
            WHERE User_ID = @User_ID
        
            INSERT INTO @UserClasses (User_ID, Distance)
                SELECT User_ID, Distance + 1
                FROM dbo.fn_UserManagers(@Manager_ID)
        
            INSERT INTO @User (User_ID, Distance) VALUES (@User_ID, 0)
        
            RETURN
        END
        

        【讨论】:

          【解决方案10】:

          您需要某种方法来防止递归查询添加已在集合中的用户 ID。但是,由于不允许对递归表进行子查询和重复提及(谢谢van),您需要另一种解决方案来删除列表中已有的用户。

          解决方案是使用 EXCEPT 删除这些行。这应该按照手册工作。允许与联合类型运算符链接的多个递归语句。删除列表中已经存在的用户意味着经过一定次数的迭代后递归结果集返回空并且递归停止。

          with UserTbl as -- Selects an employee and his subordinates.
          (
              select a.[User_ID], a.[Manager_ID] from [User] a WHERE [User_ID] = @UserID
              union all
              (
                select a.[User_ID], a.[Manager_ID] 
                  from [User] a join UserTbl b on (a.[Manager_ID]=b.[User_ID])
                  where a.[User_ID] not in (select [User_ID] from UserTbl)
                EXCEPT
                  select a.[User_ID], a.[Manager_ID] from UserTbl a 
               )
          )
          select * from UserTbl;
          

          另一种选择是硬编码一个级别变量,该变量将在固定次数的迭代后停止查询,或者使用 MAXRECURSION 查询选项提示,但我想这不是你想要的。

          【讨论】:

          • 这不起作用:CTE 中不允许多个递归引用。
          • 比我看到的更糟糕,你甚至不允许在 CTE 中使用子查询。
          猜你喜欢
          • 1970-01-01
          • 2020-04-01
          • 1970-01-01
          • 2021-12-17
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-12-19
          相关资源
          最近更新 更多