3

我有一个标准的老板/下属员工表。我需要选择一个老板(由 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:哦,如果不清楚 - 这是一个生产系统,我必须做一些升级(基本上是添加一种报告)。因此,如果可以避免,我宁愿不修改数据。

4

10 回答 10

2

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

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

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

于 2011-12-16T03:51:52.607 回答
1

我知道您前段时间问过这个问题,但这里有一个可能适用于检测无限递归循环的解决方案。我生成了一个路径,如果 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
于 2009-12-31T17:13:33.390 回答
1

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

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

于 2009-07-28T18:01:22.223 回答
1

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

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不存在,则仅将链接插入临时表,从而消除无限循环。

于 2009-07-28T10:10:31.647 回答
1

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

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
于 2009-07-28T10:40:01.933 回答
0

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

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

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
于 2009-07-28T20:02:16.677 回答
0

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

但是,您可以使用我在此处演示的多语句 UDF:避免无限循环。第一部分

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

于 2009-07-28T13:27:37.767 回答
0

我可以想到两种方法。

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 < 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
于 2009-07-28T17:11:57.537 回答
0

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

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

于 2009-07-28T09:41:33.297 回答
0

您需要某种方法来防止递归查询在集合中添加用户 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 查询选项提示,但我想这不是你想要的。

于 2009-07-28T13:52:58.827 回答