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

TSQL CTE:如何避免循环遍历?,tsql,common-table-expression,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 e

我编写了一个非常简单的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太粗糙了,不是吗


有更好的方法吗?

您需要在递归中累积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

使用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

你确定它会永远递归吗?服务器默认值为100次迭代。尝试阅读上的
MAXRECURSION
提示。首先担心效率,然后担心粗糙度,如果时间允许:)它不会永远递归,因为它在100次递归调用后抛出错误。请原谅我的措辞。我喜欢你的解决方案,因为它很有效。但是,有没有一种方法可以在没有哨兵的情况下做到这一点呢?我觉得我们必须在每个sentinel条目周围添加某种分隔符,比如sentinel='',然后我们必须在CharIndex()函数中执行同样的操作,因为如果没有分隔符,可能会出现误报。如果sentinel字符串变得太大以至于超过了varchar(max)的长度,会发生什么呢?我很高兴听到这个消息。这有点像黑客,老实说,我想不出一个“更干净”的方法。但是,请记住,sentinel沿着每个递归分支独立地增长,因此只会随着最大深度乘以每个字符串加上分隔符而变大。VARCHAR(最大值)的限制为2 GB,而最大深度可以根据需要放大到最大32767。因此,您不太可能溢出VARCHAR(MAX)。大多数递归作业可能有几千棵树,但其深度很少超过5棵左右。因此,您的sentinel字符串通常会保持相当小。我认为您必须以不同的方式构建sentinel字符串,以避免在一般情况下出现误报(当不使用CHAR(1)时)。CHARINDEX可能在
AB | C
中找到
A
,但在
中找不到
。此外,如果允许ID包含<或>,您也需要对其进行适当编码。当然,如果继续使用CHAR(1),这些都不是问题,但这不是一个现实的情况。不管怎样,我有一个好主意和+1!关于@BrankoDimitrijevic所说的,请参见。它确保sentinel中的标识符始终是分隔的,并通过查找sentinel中的
分隔符+标识符+分隔符来验证循环引用。