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

时间:2016-06-16 21:38:25

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

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

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

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

我使用两个重复的ctes完成了这个 - 一个用于获取hiearchy中每个节点的“terminal”项。然后,第二个我获得所选节点的完整祖先(因此,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完成此操作,而不必两次递归层次结构。

3 个答案:

答案 0 :(得分: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); 

答案 1 :(得分:1)

好的,这一直困扰着我,因为我已经阅读了这个问题而且我刚回来再想一想.....无论如何,你为什么需要回归以获得所有的后代?你已经要求祖先不是后代,而你的结果集并没有试图让其他兄弟姐妹,大孩子等等。在这种情况下,它会得到一个父母和一个大父母。你的第一个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 {{1 }}。 is null是我将在一分钟内处理的一个特例。

enter image description here

请记住,此时您的查询正在生成null而不是Ancestors,但它未提供给您的是自我引用,意思是descendantsgrandparent = grandparent,{{1 }}。但是,要做到这一点,您只需要为每个parent = parent添加行,并使self = self等于其id。因此parentid。现在你的结果集几乎完全形成了:

enter image description here

id的特例。因此,union标识null parentid null parentid,表示originating在您的数据集中没有其他ancestor。以下是您将如何利用这一优势。因为您在ancestor处开始了初始递归,所以与ancestor开始的leaf level没有直接联系,但是在其他所有级别上都只是劫持了这个空父级id并翻转周围的值,你现在有了你的叶子的祖先。

enter image description here

然后最后如果你想让它成为一个后代表切换列,你就完成了。如果id与其他originating ancestor一起重复,则会留下最后一个注释DISTINCT。例如。 idparentid

的另一条记录

enter image description here

答案 2 :(得分:1)

因为您的数据是树结构,我们可以使用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;

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

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而不是持久化的很多好处(你要么必须在边桌中保持最新,要么每次都生成它)。但是你去了。