SQL 层次结构 - 解析给定节点的所有祖先的完整路径

SQL Hierarchy - Resolve full path for all ancestors of a given node

我有一个由邻接表描述的层次结构。不一定有单个根元素,但我确实有数据来识别层次结构中的叶(终端)项。所以,一个看起来像这样的层次结构......

1
- 2
- - 4
- - - 7
- 3
- - 5
- - 6 
8
- 9

... 将由 table 描述,如下所示。 注意:我无法更改此格式。

id  parentid isleaf
--- -------- ------
1   null     0
2   1        0
3   1        0
4   2        0
5   3        1
6   3        1
7   4        1
8   null     0
9   8        1

这里是示例 table 定义和数据:

CREATE TABLE [dbo].[HiearchyTest](
    [id] [int] NOT NULL,
    [parentid] [int] NULL,
    [isleaf] [bit] NOT NULL
)
GO

INSERT [dbo].[HiearchyTest] ([id], [parentid], [isleaf]) VALUES (1, NULL, 0)
INSERT [dbo].[HiearchyTest] ([id], [parentid], [isleaf]) VALUES (2, 1, 0)
INSERT [dbo].[HiearchyTest] ([id], [parentid], [isleaf]) VALUES (3, 1, 0)
INSERT [dbo].[HiearchyTest] ([id], [parentid], [isleaf]) VALUES (4, 2, 0)
INSERT [dbo].[HiearchyTest] ([id], [parentid], [isleaf]) VALUES (5, 3, 1)
INSERT [dbo].[HiearchyTest] ([id], [parentid], [isleaf]) VALUES (6, 3, 1)
INSERT [dbo].[HiearchyTest] ([id], [parentid], [isleaf]) VALUES (7, 4, 1)
INSERT [dbo].[HiearchyTest] ([id], [parentid], [isleaf]) VALUES (8, NULL, 0)
INSERT [dbo].[HiearchyTest] ([id], [parentid], [isleaf]) VALUES (9, 8, 1)
GO

据此,我需要提供任何 ID 并获取所有祖先的列表,包括每个祖先的所有后代。所以,如果我提供 id = 6 的输入,我会期望以下内容:

id descendentid
-- ------------
1  1
1  3
1  6
3  3
3  6
6  6

我将使用此数据在层次结构的每个级别提供 roll-up 计算。这很好用,假设我可以得到上面的数据集。

我使用两个递归 ctes 完成了此操作 - 一个用于为层次结构中的每个节点获取 "terminal" 项。然后,在第二个节点中,我获得了我选择的节点的完整祖先(因此,6 解析为 6、3、1)以向上走并获得完整的集合。我希望我遗漏了一些东西,并且可以在一轮内完成。这是示例 double-recursion 代码:

declare @test int = 6;

with cte as (

    -- leaf nodes
    select id, parentid, id as terminalid
    from HiearchyTest
    where isleaf = 1

    union all

    -- walk up - preserve "terminal" item for all levels
    select h.id, h.parentid, c.terminalid
    from HiearchyTest as h
    inner join
    cte as c on h.id = c.parentid

)

, cte2 as (

    -- get all ancestors of our test value
    select id, parentid, id as descendentid
    from cte
    where terminalid = @test 

    union all

    -- and walkup from each to complete the set
    select h.id, h.parentid, c.descendentid
    from HiearchyTest h
    inner join cte2 as c on h.id = c.parentid

)

-- final selection - order by is just for readability of this example
select id, descendentid 
from cte2
order by id, descendentid

其他详细信息:"real" 层次结构将比示例大得多。它在技术上可以有无限的深度,但实际上它很少超过 10 级深度。

总而言之,我的问题是我是否可以使用单个递归 cte 来完成此操作,而不必在层次结构上递归两次。

我不确定这是否表现更好,甚至在所有情况下都能产生正确的结果,但您可以捕获一个节点列表,然后使用 xml 功能来解析它并交叉应用于ID 列表:

declare @test int = 6;

;WITH cte AS (SELECT id, parentid, CAST(id AS VARCHAR(MAX)) as IDlist
              FROM HiearchyTest
              WHERE isleaf = 1
              UNION ALL
              SELECT h.id, h.parentid , CAST(CONCAT(c.IDlist,',',h.id) AS VARCHAR(MAX))
              FROM HiearchyTest as h
              JOIN cte as c 
                ON  h.id = c.parentid
            )
    ,cte2 AS (SELECT *, CAST ('<M>' + REPLACE(IDlist, ',', '</M><M>') + '</M>' AS XML) AS Data 
              FROM cte
              WHERE IDlist LIKE '%'+CAST(@test AS VARCHAR(50))+'%'
              )
SELECT id,Split.a.value('.', 'VARCHAR(100)') AS descendentid
FROM cte2 a
CROSS APPLY Data.nodes ('/M') AS Split(a); 

好吧,这一直困扰着我,因为我已经阅读了这个问题,我只是回过头来再次思考它......不管怎样,你为什么需要递归回去得到所有的后代?您要求的是祖先而不是后代,并且您的结果集没有尝试获取其他兄弟姐妹、grand children 等。在这种情况下,它得到的是 parent 和 grand parent。您的第一个 cte 为您提供了您需要知道的一切,除非祖先 ID 也是 parentid。因此,使用 union all,设置原始祖先的小魔法,您无需第二次递归即可获得所需的一切。

declare @test int = 6;

with cte as (

    -- leaf nodes
    select id, parentid, id as terminalid
    from HiearchyTest
    where isleaf = 1

    union all

    -- walk up - preserve "terminal" item for all levels
    select h.id, h.parentid, c.terminalid
    from HiearchyTest as h
    inner join
    cte as c on h.id = c.parentid

)

, cteAncestors AS (

    SELECT DISTINCT
       id = IIF(parentid IS NULL, @Test, id)
       ,parentid = IIF(parentid IS NULL,id,parentid)
    FROM
       cte
    WHERE
       terminalid = @test

    UNION

    SELECT DISTINCT
       id
       ,parentid = id
    FROM
       cte
    WHERE
       terminalid = @test
) 

SELECT
    id = parentid
    ,DecendentId = id
FROM
    cteAncestors
ORDER BY
    id
    ,DecendentId

你的第一个 cte 的结果集给你你的 2 ancestors 和与他们 ancestor 相关的自我,除了 parentid is nullnull 是一个特例,我会在一分钟内处理。

请记住,此时您的查询生成的是 Ancestors 而不是 descendants,但它没有给您的是自我引用,意思是 grandparent = grandparentparent = parentself = self。但是你要做的就是为每个 id 添加行并使 parentid 等于它们的 id。因此 union。现在您的结果集几乎完全成形了:

null parentid的特例。因此 null parentid 标识 originating ancestor 意味着 ancestor 在您的数据集中没有其他 ancestor。以下是您将如何利用它来发挥自己的优势。因为您在 leaf level 开始了初始递归,所以与您开始与 originating ancestorid 没有直接联系,但在其他所有级别都有,只需劫持 null parent id 并翻转值,你现在有了你的叶子的祖先。

最后,如果您希望它成为后代,table 切换列,您就完成了。最后一个音符 DISTINCT 是为了防止 id 与额外的 parentid 重复。例如。 6 | 36 | 4

的另一条记录

因为你的数据是树状结构,我们可以使用hierarchyid数据类型来满足你的需求(尽管你在评论里说你不能)。首先,简单的部分 - 使用递归 cte

生成 hierarchyid
with cte as (

    select id, parentid, 
       cast(concat('/', id, '/') as varchar(max)) as [path]
    from [dbo].[HiearchyTest]
    where ParentID is null

    union all

    select child.id, child.parentid, 
       cast(concat(parent.[path], child.id, '/') as varchar(max))
    from [dbo].[HiearchyTest] as child
    join cte as parent
        on child.parentid = parent.id
)
select id, parentid, cast([path] as hierarchyid) as [path] 
into h
from cte;

接下来,我写了一个 table 值函数:

create function dbo.GetAllAncestors(@h hierarchyid, @ReturnSelf bit)
returns table
as return
   select @h.GetAncestor(n.n) as h
   from dbo.Numbers as n
   where n.n <= @h.GetLevel()
      or (@ReturnSelf = 1 and n.n = 0)

   union all

   select @h
   where @ReturnSelf = 1;

有了这些,得到你想要的结果集也不错:

declare @h hierarchyid;

set @h = (
    select path
    from h
    where id = 6
);

with cte as (
    select * 
    from h
    where [path].IsDescendantOf(@h) = 1
        or @h.IsDescendantOf([path]) = 1
)
select h.id as parent, c.id as descendentid
from cte as c
cross apply dbo.GetAllAncestors([path], 1) as a
join h
    on a.h = h.[path]
order by h.id, c.id;

当然,您会因为不持久化而错过使用 hierarchyid 的很多好处(您要么必须在 table 旁边保持最新状态,要么生成它每次)。但是你去吧。