假设您有一个存储有序树层次结构的平面表:
Id Name ParentId Order
1 'Node 1' 0 10
2 'Node 1.1' 1 10
3 'Node 2' 0 20
4 'Node 1.1.1' 2 10
5 'Node 2.1' 3 10
6 'Node 1.2' 1 20
这是一个图表,其中我们有[id] Name
。根节点 0 是虚构的。
[0] ROOT / \ [1] Node 1 [3] Node 2 / \ \ [2] Node 1.1 [6] Node 1.2 [5] Node 2.1 / [4] Node 1.1.1
您将使用哪种简约方法将其作为正确排序、正确缩进的树输出到 HTML(或文本,就此而言)?
进一步假设您只有基本的数据结构(数组和 HashMap ),没有带有父/子引用的奇特对象,没有 ORM,没有框架,只有您的两只手。该表表示为一个结果集,可以随机访问。
伪代码或者简单的英文都行,这纯粹是个概念题。
红利问题:有没有更好的方法来在 RDBMS 中存储这样的树结构?
编辑和添加
回答一位评论者(Mark Bessey 的)问题:根节点不是必需的,因为它永远不会显示。 ParentId = 0 是表达“这些是顶级”的约定。 Order 列定义了如何对具有相同父节点的节点进行排序。
我所说的“结果集”可以被描绘成 HashMap 数组(保留在该术语中)。因为我的例子本来就已经存在了。有些答案更进一步并首先构建它,但这没关系。
树可以任意深。每个节点可以有 N 个 child 。不过,我并没有完全想到“数百万条目”树。
不要将我选择的节点命名(“节点 1.1.1”)误认为是可以依赖的东西。这些节点同样可以称为“Frank”或“Bob”,没有暗示命名结构,这只是为了使其可读。
我已经发布了我自己的解决方案,所以你们可以把它分解成碎片。
最佳答案
现在MySQL 8.0 supports recursive queries ,我们可以说 all popular SQL databases support recursive queries在标准语法中。
WITH RECURSIVE MyTree AS (
SELECT * FROM MyTable WHERE ParentId IS NULL
UNION ALL
SELECT m.* FROM MyTABLE AS m JOIN MyTree AS t ON m.ParentId = t.Id
)
SELECT * FROM MyTree;
我在演示文稿中测试了 MySQL 8.0 中的递归查询 Recursive Query Throwdown 2017 年。
以下是我 2008 年的原始回答:
有几种方法可以在关系数据库中存储树结构数据。您在示例中显示的内容使用两种方法:
- 邻接列表(“父”列)和
- 路径枚举(您的姓名列中的虚线数字)。
另一种解决方案称为嵌套集,它也可以存储在同一个表中。阅读 Joe Celko 的“Trees and Hierarchies in SQL for Smarties”,了解有关这些设计的更多信息。
我通常更喜欢一种称为闭包表(又名“邻接关系”)的设计来存储树结构数据。它需要另一个表,但是查询树非常容易。
我在演示文稿中介绍了闭包表 Models for Hierarchical Data with SQL and PHP在我的书中 SQL Antipatterns Volume 1: Avoiding the Pitfalls of Database Programming .
CREATE TABLE ClosureTable (
ancestor_id INT NOT NULL REFERENCES FlatTable(id),
descendant_id INT NOT NULL REFERENCES FlatTable(id),
PRIMARY KEY (ancestor_id, descendant_id)
);
将所有路径存储在闭包表中,其中存在从一个节点到另一个节点的直接祖先。为每个节点包含一行以引用自身。例如,使用您在问题中显示的数据集:
INSERT INTO ClosureTable (ancestor_id, descendant_id) VALUES
(1,1), (1,2), (1,4), (1,6),
(2,2), (2,4),
(3,3), (3,5),
(4,4),
(5,5),
(6,6);
现在你可以像这样得到一棵从节点 1 开始的树:
SELECT f.*
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1;
输出(在 MySQL 客户端中)如下所示:
+----+
| id |
+----+
| 1 |
| 2 |
| 4 |
| 6 |
+----+
换句话说,节点 3 和 5 被排除在外,因为它们是单独层次结构的一部分,而不是从节点 1 下降。
回复:来自 e-satis 关于直系子女(或直系 parent )的评论。您可以将“path_length
”列添加到 ClosureTable
中,以便更轻松地专门查询直系子级或父级(或任何其他距离)。
INSERT INTO ClosureTable (ancestor_id, descendant_id, path_length) VALUES
(1,1,0), (1,2,1), (1,4,2), (1,6,1),
(2,2,0), (2,4,1),
(3,3,0), (3,5,1),
(4,4,0),
(5,5,0),
(6,6,0);
然后您可以在搜索中添加一个术语以查询给定节点的直接子节点。这些是 path_length
为 1 的后代。
SELECT f.*
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1
AND path_length = 1;
+----+
| id |
+----+
| 2 |
| 6 |
+----+
来自@ashraf 的评论:“[按名称] 对整棵树进行排序怎么样?”
这是一个示例查询,用于返回节点 1 的所有后代节点,将它们连接到包含其他节点属性(例如 name
)的 FlatTable,并按名称排序。
SELECT f.name
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1
ORDER BY f.name;
来自@Nate 的评论:
SELECT f.name, GROUP_CONCAT(b.ancestor_id order by b.path_length desc) AS breadcrumbs
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
JOIN ClosureTable b ON (b.descendant_id = a.descendant_id)
WHERE a.ancestor_id = 1
GROUP BY a.descendant_id
ORDER BY f.name
+------------+-------------+
| name | breadcrumbs |
+------------+-------------+
| Node 1 | 1 |
| Node 1.1 | 1,2 |
| Node 1.1.1 | 1,2,4 |
| Node 1.2 | 1,6 |
+------------+-------------+
今天有用户建议修改。所以版主批准了编辑,但我正在撤销它。
编辑建议上面最后一个查询中的 ORDER BY 应该是 ORDER BY b.path_length, f.name
,大概是为了确保顺序与层次结构匹配。但这不起作用,因为它会在“节点 1.2”之后排序“节点 1.1.1”。
如果您希望排序以合理的方式匹配层次结构,那是可能的,但不能简单地按路径长度排序。例如,请参阅我对 MySQL Closure Table hierarchical database - How to pull information out in the correct order 的回答.
关于sql - 将平面表解析为树的最有效/优雅的方法是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/192220/