编程数据工程师

如何在SQL中有效地处理和存储层次(树状)数据?有哪些方法可用,如何选择适合任务的方法?

用 Hintsage AI 助手通过面试

回答。

处理层次结构是关系数据库的经典应用。示例:目录、树状菜单、组织结构。

问题的历史: 数据库是表格模型。对于树状数据,有几种方法,每种都有其优缺点。

问题: 标准的parent_id模型导致递归SELECT变慢。实际任务(查找所有后代、路径、子树计数)需要优化。

解决方案:

  • 邻接列表 — 对parent_id的简单引用。
  • 物化路径 — 反映完整路径的字符串。
  • 嵌套集合 — 存储左/右边界(left/right),便于提取子树。
  • 闭包表 — 一个反映树中所有关系的独立关系表(from->to)。

物化路径的代码示例(PostgreSQL):

CREATE TABLE categories ( id SERIAL PRIMARY KEY, name TEXT, path TEXT ); -- 示例路径存储:'1/2/5/'(根/子类别/当前) SELECT * FROM categories WHERE path LIKE '1/2/%'; -- 所有后代 2

嵌套集合的代码示例:

CREATE TABLE nested_categories ( id SERIAL PRIMARY KEY, name TEXT, lft INT NOT NULL, rgt INT NOT NULL ); -- 子节点 SELECT * FROM nested_categories WHERE lft > 2 AND rgt < 15;

关键特点:

  • 邻接列表适合简单的树状结构(低嵌套性)。
  • 物化路径 — 快速提取子树,易于实现。
  • 嵌套集合 — 即时获取所有后代,快速读取,但修改复杂。

迷惑性问题。

可以简单使用带有parent_id的嵌套SELECT按任意深度查找所有后代吗?

这只适用于浅层查询。递归搜索需要递归CTE (WITH RECURSIVE) 或更复杂的方案,因为简单的JOIN会导致巨大的调用次数和低性能。

示例:

WITH RECURSIVE cte AS ( SELECT id, parent_id, name FROM categories WHERE id = 1 UNION ALL SELECT c.id, c.parent_id, c.name FROM categories c INNER JOIN cte ON c.parent_id = cte.id ) SELECT * FROM cte;

为什么分子树(嵌套集合)快速提取子树,但添加/删除节点慢?

在更改树时,很多行的lft/rgt需要同时更改(更改步骤是所有比插入/删除大的值)。对于读取来说,方法是理想的,但频繁更改时请使用其他方法。

何时使用闭包表而不是parent_id或物化路径?

闭包表非常适合对任意层级的后代进行多次查询和定期重新计算关系。缺点是需要更多空间。

示例:

CREATE TABLE closure ( ancestor INT, descendant INT, depth INT );

常见错误和反模式

  • 仅通过parent_id存储层级,而需要快速查找嵌套结构。
  • 手动重新计算路径或lft/rgt而没有辅助函数。
  • 缺少关键列的索引(parent_id/path/lft/rgt)。

生活中的示例

消极案例

在网络商店中,类别通过parent_id实现。所有嵌套项手动设置,子类别的查找通过嵌套SELECT进行。

优点:

  • 简单,不需要扩展支持。

缺点:

  • 在3-4个层级的嵌套时性能下降。
  • 节点移动操作不明显,导致逻辑错误。

积极案例

使用物化路径或闭包表。对嵌套类别的所有查询都是瞬间完成,重新计算使用批量脚本。

优点:

  • 可扩展性。
  • 快速的嵌套选择。

缺点:

  • 需要额外的规划。
  • 在结构变更时增加负载。