【问题标题】:SQL Hierarchy - Resolve full path for all ancestors of a given nodeSQL Hierarchy - 解析给定节点的所有祖先的完整路径
【发布时间】:2016-10-18 14:15:48
【问题描述】:

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

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

... 会用一个表格来描述,就像这样。 注意:我无法更改此格式。

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

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

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
  • id 6 有它自己
  • 它的父代 id 3 的后代是 3 和 6
  • 其父代 id 1 的后代为 1、3 和 6

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

我已经使用两个 recusive ctes 完成了这个 - 一个用于获取层次结构中每个节点的“终端”项。然后,第二个我得到我选择的节点的完整祖先(所以,6 解析为 6、3、1)走上去得到完整的集合。我希望我遗漏了一些东西,并且这可以在一轮中完成。这是示例双递归代码:

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

附加细节:“真实”层次结构将比示例大得多。从技术上讲,它可以有无限的深度,但实际上它很少会超过 10 层。

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

【问题讨论】:

  • msdn.microsoft.com/en-us/library/bb677173.aspx 这是微软关于一些新的层次结构函数的文章,正是针对这一点的。
  • @Matt - 感谢您的建议,但我上面的注释指出我无法更改数据结构(它不是我的表)。所以,我没有能力添加一个 hierarchyid 列。此外,根据我对层次结构 ID 的经验,我还没有看到一种方法可以满足我的要求。您会注意到,根据我输入的 id = 6,我需要该 id 的所有祖先的完整后代列表。
  • 知道了,我读得很快 :) 递归 cte 很可能是我会再读一遍的路线,看看它是否能帮上忙。
  • 我认为你有正确的方法,但有兴趣看看是否有人有一些巧妙的解决方案来绕过两个递归 cte。如果这是一项足够常见且足够昂贵的任务,您可以为每条记录预先构建一个新表,但这可能不可行。
  • 如果您只是在第一个 CTE 中寻找简单的东西,而不是“保留”终端 ID,为什么不保留路径?例如cast(id as varchar(4000)) as teminalid 在 cte 的第一部分,然后在它的第二部分 cast(h.id as varchar(4000)) + '/' + cast(c.terminalid as varchar(4000))。或者这个想法的一个变种,取决于你的需要。之后,您只需要在第一个 cte 中检查带有 charindex('6', terminalid) > 0 的任何内容,并且您将拥有每个人的完整路径,除非我遗漏了什么。

标签: sql sql-server sql-server-2014 common-table-expression hierarchical-data


【解决方案1】:

我不确定这是否表现更好,甚至在所有情况下都能产生正确的结果,但您可以捕获节点列表,然后使用 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); 

【讨论】:

  • 有趣的方法。它似乎确实有效。我接受马特的回答仅仅是因为它对我来说似乎更直接并且更接近我所寻找的。然而,这似乎是一种有效的方法,我赞成你的回答。
【解决方案2】:

好吧,自从我阅读了这个问题之后,这一直困扰着我,我刚刚回来再次想到它......无论如何,你为什么需要递归回去以获得所有后代?您要求的是祖先而不是后代,并且您的结果集不是试图获得其他兄弟姐妹、孙辈等。在这种情况下,它正在获得父母和祖父母。您的 First 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 开始了您的初始递归,所以与您从id 开始的id 没有直接联系,但是在每个其他级别,只需劫持该空父ID 并将值翻转现在你的叶子有了一个祖先。

最后,如果您希望它成为后代表,请切换列,您就完成了。最后一个注释DISTINCTs 存在,以防id 与额外的parentid 重复。例如。 6 | 36 | 4 的另一条记录

【讨论】:

    【解决方案3】:

    因为你的数据是树形结构,我们可以使用hierarchyid数据类型来满足你的需求(尽管你说在cmets中不行)。首先,简单的部分 - 使用递归 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;
    

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

    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 的许多好处(您要么必须在边表中保持它是最新的,要么每次都生成它)。但是你去吧。

    【讨论】:

    • 当然值得一票。我最终接受了马特的答案,因为它更接近我想要的答案,但我也会和你一起玩,尽可能多地学习。谢谢。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-01-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多