TSQL CTE:如何避免循环遍历?

时间:2012-06-14 21:45:05

标签: tsql common-table-expression

我编写了一个非常简单的CTE表达式,用于检索用户所属的所有组的列表。

规则是这样的,用户可以在多个组中,并且组可以嵌套,以便组可以是另一个组的成员,此外,组可以是另一个组的共同成员,因此组A是B组和B组成员也是A组的成员。

我的CTE是这样的,显然它会产生无限的递归:

            ;WITH GetMembershipInfo(entityId) AS( -- entity can be a user or group
                SELECT k.ID as entityId FROM entities k WHERE k.id = @userId
                UNION ALL
                SELECT k.id FROM entities k 
                JOIN Xrelationships kc on kc.entityId = k.entityId
                JOIN GetMembershipInfo m on m.entityId = kc.ChildID
            )

我无法找到一个简单的解决方案来回溯我已记录的那些群组。

我在考虑在CTE中使用额外的varchar参数来记录我访问过的所有组的列表,但是使用varchar太粗糙了,不是吗?

有更好的方法吗?

2 个答案:

答案 0 :(得分:25)

您需要在递归中累积一个sentinel字符串。在下面的例子中,我有一个从A,B,C,D到A的循环关系,我避免了带有sentinel字符串的循环:

DECLARE @MyTable TABLE(Parent CHAR(1), Child CHAR(1));

INSERT @MyTable VALUES('A', 'B');
INSERT @MyTable VALUES('B', 'C');
INSERT @MyTable VALUES('C', 'D');
INSERT @MyTable VALUES('D', 'A');

; WITH CTE (Parent, Child, Sentinel) AS (
    SELECT  Parent, Child, Sentinel = CAST(Parent AS VARCHAR(MAX))
    FROM    @MyTable
    WHERE   Parent = 'A'
    UNION ALL
    SELECT  CTE.Child, t.Child, Sentinel + '|' + CTE.Child
    FROM    CTE
    JOIN    @MyTable t ON t.Parent = CTE.Child
    WHERE   CHARINDEX(CTE.Child,Sentinel)=0
)
SELECT * FROM CTE;

结果:

Parent Child Sentinel
------ ----- --------
A      B     A
B      C     A|B
C      D     A|B|C
D      A     A|B|C|D

答案 1 :(得分:2)

使用sentinel表变量代替sentinel字符串。无论圆圈有多少跳跃,函数都会捕获循环引用,nvarchar(max)的最大长度没有问题,可以根据不同的数据类型甚至多部分键轻松修改,并且可以将函数分配给检查约束。

CREATE FUNCTION [dbo].[AccountsCircular] (@AccountID UNIQUEIDENTIFIER)
RETURNS BIT 
AS
BEGIN
    DECLARE @NextAccountID UNIQUEIDENTIFIER = NULL;
    DECLARE @Sentinel TABLE
    (
        ID UNIQUEIDENTIFIER
    )
    INSERT INTO     @Sentinel
                ( [ID] )
    VALUES          ( @AccountID )
    SET @NextAccountID = @AccountID;

    WHILE @NextAccountID IS NOT NULL
    BEGIN
        SELECT  @NextAccountID = [ParentAccountID]
        FROM    [dbo].[Accounts]
        WHERE   [AccountID] = @NextAccountID;
        IF  EXISTS(SELECT 1 FROM @Sentinel WHERE ID = @NextAccountID)
            RETURN 1;
        INSERT INTO @Sentinel
                ( [ID] )
        VALUES      ( @NextAccountID )
    END
    RETURN 0;
END