使用公用表表达式避免重复递归

Posted

技术标签:

【中文标题】使用公用表表达式避免重复递归【英文标题】:Avoiding duplicate recursion with Common Table Expressions 【发布时间】:2015-03-19 21:40:36 【问题描述】:

假设我有一个包含 2 列 ID 和 ParentID 的表。我的数据如下所示:

ID           ParentID
1            Null
2            1
3            1
4            2
4            2

因此,要根据给定 ID 查找所有关系,我的简化查询如下所示:

WITH links ([ID], [ParentID], Depth)
AS
(
    --Get the starting link
    SELECT 
        [ID],
        [ParentID],
        [Depth] = 1
    FROM
        [MyTable] 
    WHERE
        [ID] = @StartID

    UNION ALL

    --Recursively get links that are parented to links already in the CTE
    SELECT 
        mt.[ID],
        mt.[ParentID],
        [Depth] = l.[Depth] + 1
    FROM
        [MyTable] mt
    JOIN
        links l ON mt.ParentID = l.ID
    WHERE
        Depth < 99
)
SELECT 
    [Depth],
    [ID],
    [ParentID]
FROM
    [links]

现在假设我的表中的数据创建了一个循环关系(4 是 2 的父级,2 是 4 的父级。暂时忘记了数据库可能应该有约束来防止这种情况,上面的递归 CTE 查询产生重复记录(其中 99 个),因为它会递归地评估 2 和 4 之间的循环关系。

ID           ParentID
1            Null
2            1
3            1
4            2
2            4
2            4

假设我无法控制阻止实际数据表示该循环关系,我该如何更改我的查询以防止这种情况发生。通常我会在最终选择上放置一个不同的值,但我想要 Depth 值,这使得每条记录都不同。我也希望在 CTE 中考虑它,因为 distinct 对最终选择进行操作,并且可能效率不高。

【问题讨论】:

【参考方案1】:

您可以在 CTE 中创建一个树路径变量,该变量显示您从递归查询顶部开始的整个路径,然后检查有问题的数字是否在树路径中,如果它在该点中止。

USE Master;
GO
CREATE DATABASE [QueryTraining];
GO
USE [QueryTraining];
GO

CREATE TABLE [MyTable] (
    ID int,  --would normally be an INT IDENTITY
    ParentID int
    );

INSERT INTO [MyTable] (ID, ParentID) 
        VALUES (1, NULL),
               (2, 1),
               (3, 1),
               (4, 2),
               (2, 4),
               (2, 4);

DECLARE @StartID AS INTEGER;
SET @StartID = 1;

;WITH links (ID, ParentID, Depth, treePath)
AS
(
    --Get the starting link
    SELECT [ID],
           [ParentID],
           [Depth] = 1, 
           CAST(':' + CAST([ID] AS VARCHAR(MAX)) AS VARCHAR(MAX)) AS treePath
      FROM [MyTable] 
     WHERE [ID] = @StartID

    UNION ALL

    --Recursively get links that are parented to links already in the CTE
    SELECT mt.[ID],
           mt.[ParentID],
           [Depth] = l.[Depth] + 1,
           CAST(l.treePath + CAST(mt.[ID] AS VARCHAR(MAX)) + ':' AS VARCHAR(MAX)) AS treePath
      FROM [MyTable] mt
     INNER JOIN links l ON mt.ParentID = l.ID
       AND CHARINDEX(':' + CAST(mt.[ID] AS VARCHAR(MAX)) + ':', l.[treePath]) = 0
     WHERE Depth < 10
)
SELECT 
    [Depth],
    [ID],
    [ParentID],
    [treePath]
FROM
    [links];

INNER JOIN 上的那一行 AND CHARINDEX(':' + CAST(mt.[ID] AS VARCHAR(MAX)) + ':', l.[treePath]) = 0 是路径中之前的数字被过滤掉的地方。

只需复制并粘贴示例并尝试一下。

请注意,我在 CTE 上使用 CHARINDEX 的方式可能无法很好地扩展,但它确实实现了我认为您正在寻找的东西。

【讨论】:

以上是关于使用公用表表达式避免重复递归的主要内容,如果未能解决你的问题,请参考以下文章

SQL Server 公用表表达式(CTE)实现递归

sql server数据库性能优化之2-避免使用CTE公用表达式的递归by zhang502219048

MySQL 中不允许在递归公用表表达式中使用 LIMIT

SQL-CTE公用表达式

SQL递归查询知多少

使用公用表表达式和相关更新标记同一表中的重复数据