【问题标题】:TSQL CTE: How to avoid circular traversal?TSQL CTE:如何避免循环遍历?
【发布时间】:2012-06-14 21:45:04
【问题描述】:

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

有没有更好的办法?

【问题讨论】:

  • 你确定它是永远递归的吗?服务器默认值为 100 次迭代。尝试阅读MSDN 上的MAXRECURSION 提示。
  • 先担心功效,然后担心粗糙,如果时间允许的话:)
  • 它不会永远递归,因为它会在 100 次递归调用后引发错误。原谅我的措辞。

标签: tsql common-table-expression


【解决方案1】:

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

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 = '' 然后我们必须在 CharIndex( ) 函数,因为没有分隔符可能会出现误报。如果哨兵字符串变得如此之大以至于超过了 varchar(max) 的长度会发生什么?
  • 我很高兴听到这个作品。这有点骇人听闻,老实说,我想不出一种“更清洁”的方式。但是,请记住,哨兵沿着每个递归分支独立增长,因此只会变得与每个字符串的最大深度乘以分隔符一样大。 VARCHAR(MAX) 的限制为 2 GB,而最大深度可以根据需要扩大到最大 32767。因此,您不太可能溢出 VARCHAR(MAX)。大多数递归作业可能有几千棵树,但它们的深度很少超过 5 左右。因此,您的标记字符串通常会保持相当小。
  • 我认为您必须以不同的方式构建标记字符串以避免在一般情况下出现误报(不使用 CHAR(1) 时)。 CHARINDEX 可能会在 AB|C 中找到 A,但在 <AB><C> 中找不到 <A>。此外,如果允许 ID 包含 ,您也需要对其进行正确编码。当然,如果你只是继续使用 CHAR(1),这都不是问题,但这不是一个现实的情况。无论如何,好主意和我的 +1!
  • 关于@BrankoDimitrijevic 所说的内容,请参阅this solution。它确保标记中的标识符始终被分隔,并通过在标记中查找delimiter + identifier + delimiter 来验证循环引用。
【解决方案2】:

使用哨兵表变量代替哨兵字符串。无论圆有多少跳,函数都会捕获循环引用,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

【讨论】:

    猜你喜欢
    • 2021-09-27
    • 1970-01-01
    • 2018-08-02
    • 2021-08-20
    • 1970-01-01
    • 1970-01-01
    • 2017-04-15
    • 2018-03-13
    • 2021-12-02
    相关资源
    最近更新 更多