将邻接列表层次结构展平为所有路径的列表
Posted
技术标签:
【中文标题】将邻接列表层次结构展平为所有路径的列表【英文标题】:Flatten Adjacency List Hierarchy To A List Of All Paths 【发布时间】:2010-10-20 08:29:03 【问题描述】:我有一个使用邻接列表模型存储层次信息的表。 (使用自引用键 - 下面的示例。此表可能看起来像 familiar):
category_id name parent
----------- -------------------- -----------
1 ELECTRONICS NULL
2 TELEVISIONS 1
3 TUBE 2
4 LCD 2
5 PLASMA 2
6 PORTABLE ELECTRONICS 1
7 MP3 PLAYERS 6
8 FLASH 7
9 CD PLAYERS 6
10 2 WAY RADios 6
将上述数据“扁平化”成这样的最佳方法是什么?
category_id lvl1 lvl2 lvl3 lvl4
----------- ----------- ----------- ----------- -----------
1 1 NULL NULL NULL
2 1 2 NULL NULL
6 1 6 NULL NULL
3 1 2 3 NULL
4 1 2 4 NULL
5 1 2 5 NULL
7 1 6 7 NULL
9 1 6 9 NULL
10 1 6 10 NULL
8 1 6 7 8
每一行都是通过层次结构的一个“路径”,除了每个节点(不仅仅是每个叶节点)都有一行。 category_id 列代表当前节点,“lvl”列是它的祖先。当前节点的值也必须在最右边的 lvl 列中。 lvl1 列中的值将始终表示根节点,lvl2 中的值将始终表示 lvl1 的直接后代,依此类推。
如果可能,生成此输出的方法应使用 SQL,并且适用于 n 层层次结构。
【问题讨论】:
对于 n 层层次结构:是否提前知道 n? 没有。我希望解决方案足够通用以适用于任何层次结构。但是 - 如果 'n' 已知,是否有更优雅的解决方案? 【参考方案1】:在一个简单的邻接列表中进行多级查询总是涉及到自左连接。制作右对齐表格很容易:
SELECT category.category_id,
ancestor4.category_id AS lvl4,
ancestor3.category_id AS lvl3,
ancestor2.category_id AS lvl2,
ancestor1.category_id AS lvl1
FROM categories AS category
LEFT JOIN categories AS ancestor1 ON ancestor1.category_id=category.category_id
LEFT JOIN categories AS ancestor2 ON ancestor2.category_id=ancestor1.parent
LEFT JOIN categories AS ancestor3 ON ancestor3.category_id=ancestor2.parent
LEFT JOIN categories AS ancestor4 ON ancestor4.category_id=ancestor3.parent;
像你的例子一样左对齐有点棘手。想到这个:
SELECT category.category_id,
ancestor1.category_id AS lvl1,
ancestor2.category_id AS lvl2,
ancestor3.category_id AS lvl3,
ancestor4.category_id AS lvl4
FROM categories AS category
LEFT JOIN categories AS ancestor1 ON ancestor1.parent IS NULL
LEFT JOIN categories AS ancestor2 ON ancestor1.category_id<>category.category_id AND ancestor2.parent=ancestor1.category_id
LEFT JOIN categories AS ancestor3 ON ancestor2.category_id<>category.category_id AND ancestor3.parent=ancestor2.category_id
LEFT JOIN categories AS ancestor4 ON ancestor3.category_id<>category.category_id AND ancestor4.parent=ancestor3.category_id
WHERE
ancestor1.category_id=category.category_id OR
ancestor2.category_id=category.category_id OR
ancestor3.category_id=category.category_id OR
ancestor4.category_id=category.category_id;
适用于 n 层层次结构。
抱歉,在邻接列表模型中无法进行任意深度的查询。如果您经常执行此类查询,则应将架构更改为 other models of storing hierarchical information 之一:完全邻接关系(存储所有祖先-后代关系)、物化路径或嵌套集。
如果类别不经常移动(对于像您的示例这样的商店来说通常是这种情况),我会倾向于嵌套集合。
【讨论】:
感谢您的回答。如果数据是使用嵌套集模型存储的,有没有比上面给出的第二个选项更好的方法来获得这个输出? 另外,有什么想法可以提高上面第二个查询的性能吗? 在搜索其他内容时遇到此问题,并想更正一些信息。在使用递归的邻接表模型中可以进行任意深度查询。以 SQL Server 为例,您可以使用 CTE 递归检索所有后代。【参考方案2】:如前所述,SQL 没有清晰的方法来实现具有动态变化的列数的表。我之前使用过的仅有的两个解决方案是: 1. 固定数量的 Self-Joins,给出固定数量的列(AS per BobInce) 2. 将结果生成为单个列中的字符串
第二个听起来很怪诞;将 ID 存储为字符串?!但是当输出被格式化为 XML 什么的时候,人们似乎并没有那么在意。
同样,如果您想加入 SQL 中的结果,这也没什么用。如果要将结果提供给应用程序,它可能非常合适。然而,就个人而言,我更喜欢在应用程序中进行扁平化而不是 SQL
我被困在一个 10 英寸的屏幕上,无法访问 SQL,所以我无法提供测试代码,但基本方法是以某种方式利用递归; - 递归标量函数可以做到这一点 - MS SQL 可以使用递归 WITH 语句来做到这一点(更高效)
标量函数(类似):
CREATE FUNCTION getGraphWalk(@child_id INT)
RETURNS VARCHAR(4000)
AS
BEGIN
DECLARE @graph VARCHAR(4000)
-- This step assumes each child only has one parent
SELECT
@graph = dbo.getGraphWalk(parent_id)
FROM
mapping_table
WHERE
category_id = @child_id
AND parent_id IS NOT NULL
IF (@graph IS NULL)
SET @graph = CAST(@child_id AS VARCHAR(16))
ELSE
SET @graph = @graph + ',' + CAST(@child_id AS VARCHAR(16))
RETURN @graph
END
SELECT
category_id AS [category_id],
dbo.getGraphWalk(category_id) AS [graph_path]
FROM
mapping_table
ORDER BY
category_id
我已经有一段时间没有使用递归 WITH了,但我会尝试一下语法,即使我这里没有 SQL 来测试任何东西:)
递归与
WITH
result (
category_id,
graph_path
)
AS
(
SELECT
category_id,
CAST(category_id AS VARCHAR(4000))
FROM
mapping_table
WHERE
parent_id IS NULL
UNION ALL
SELECT
mapping_table.category_id,
CAST(result.graph_path + ',' + CAST(mapping_table.category_id AS VARCHAR(16)) AS VARCHAR(4000))
FROM
result
INNER JOIN
mapping_table
ON result.category_id = mapping_table.parent_id
)
SELECT
*
FROM
result
ORDER BY
category_id
编辑 - 两者的输出相同:
1 '1'
2 '1,2'
3 '1,2,3'
4 '1,2,4'
5 '1,2,5'
6 '1,6'
7 '1,6,7'
8 '1,6,7,8'
9 '1,6,9'
【讨论】:
谢谢。这两种方法都有效(除了您上面提到的差异),但 SQL 确实需要一些调整。如果你有机会,请更新它(并请包括输出)。我会,但我还不能编辑你的答案。【参考方案3】:遍历任意深度的树通常涉及递归过程代码,除非您利用某些 DBMS 的特殊功能。
在 Oracle 中,如果您使用邻接表,CONNECT BY 子句将允许您以第一顺序深度遍历树,就像您在此处所做的那样。
如果你使用嵌套集合,左边的序号会为你提供访问节点的顺序。
【讨论】:
【参考方案4】:实际上可以在存储过程中使用动态 SQL 来完成。然后,您将受限于存储过程可以做的事情。显然,将结果执行到一个不知道有多少列的临时表中是一个挑战。但是,如果目标是输出到网页或其他 UI,那么这可能是值得的……
【讨论】:
以上是关于将邻接列表层次结构展平为所有路径的列表的主要内容,如果未能解决你的问题,请参考以下文章