在日常系统开发中,树形结构是一种非常常见的数据组织方式,比如常见的功能树、人员树、组织架构树等。很多开发者在处理这类结构时,通常使用 parent_id 来表示父子关系,这种方式虽简单,但当面对大数据量、复杂查询、统计分析等需求时,性能和扩展性都会面临挑战。

今天,我们就来学习一种非常实用且在实际业务中非常高效的算法:预排序模型(Nested Set Model),它能极大提升树形结构的查询和统计效率,尤其适用于那些层级稳定、读操作频繁的业务场景。


一、树形结构的常规建模方式

我们以组织架构树为例。典型的表结构如下:

字段名含义
id部门ID(主键)
name部门名称
parent_id上级部门ID
level所在层级
parent_path从根节点到当前节点的路径,如 1-3-7

这是很多系统常用的建模方式,通过 parent_id 构建出部门之间的上下级关系,通过 parent_path 辅助做路径查询。


二、基于 Parent ID 的优缺点分析

优点:

  1. 实现简单:只需维护 parent_id 即可构造出完整的树形结构。
  2. 插入灵活:新增节点只需要提供父节点 ID,不需要更新其他记录。
  3. 祖先路径可追踪:通过递归查询或 parent_path 字段可方便获取祖先信息。

问题与瓶颈:

虽然实现简单,但随着数据量增大,性能瓶颈也随之而来:

  • 全树或局部遍历很慢:想获取某个节点下的所有子节点(尤其是孙子、曾孙节点)时,无法一次性查询,必须递归。
  • 层级统计困难:如统计部门 A 下有多少个直属子部门、三级部门、所有子孙节点等,很难用 SQL 快速实现。
  • 使用 CTE 仍不够快:即使用 MySQL 的公共表达式(WITH RECURSIVE),性能依旧堪忧。
  • 缺乏全树结构信息:只能从子节点向上追溯,无法从上向下“扫”完整的子树。

这些缺点在层级多、数据量大的场景下会被放大,迫切需要更高效的树结构处理方式。


三、预排序模型(Nested Set Model)介绍

为了提升查询效率,我们可以为每个节点额外增加两个字段:left 和 right。

核心思想:

通过深度优先遍历整棵树,在每个节点“进入”和“离开”时分别记录一个编号,从而为节点赋予 left 和 right 值。

字段名含义
left遍历进入节点时的顺序号
right遍历离开节点时的顺序号

这种方式形成了一个预先计算好遍历顺序的树结构,因此被称为“预排序模型”。


四、构建过程详解:先深度后广度,编号递增

我们以一个组织树为例:

- A(总公司)
  - B(运营部)
    - D(用户组)
    - E(产品组)
    - F(推广组)
  - C(技术部)
    - G(开发组)
    - H(测试组)

构建流程如下:

  1. 从根节点 A 开始,left=1;
  2. 进入子节点 B,left=2;
  3. 再进入 B 的子节点 D,left=3,没有下级,right=4;
  4. 返回 B 的下一个子节点 E,left=5,无下级,right=6;
  5. 同理 F 的 left=7,right=8;
  6. B 的所有子节点处理完毕,right=9;
  7. C 的子节点 G、H 同理编号为 10~15;
  8. A 最后 right=16。

编号如下所示:

部门leftright
A116
B29
D34
E56
F78
C1015
G1112
H1314

五、预排序模型的强大查询能力

1. 查询某部门下的所有子孙节点(不限制层级)

SELECT * FROM dept WHERE left > 2 AND right < 9;

解释:left 和 right 在 B 节点的范围内,表示所有子孙节点(D、E、F)。


2. 查询直属子部门

SELECT * FROM dept 
WHERE left > 2 AND right < 9 AND level = 2 + 1;

level 为上级 level+1,筛出直属子部门。


3. 统计所有子孙节点的数量

公式:(right - left - 1) / 2

如:B 节点 → (9 - 2 - 1)/2 = 3,即 D、E、F 三个子部门。


4. 判断是否为叶子节点

WHERE right = left + 1;

说明无子节点,如 D、E、F 都满足。


5. 查询某节点的所有上级(祖先)部门

SELECT * FROM dept 
WHERE left < 5 AND right > 6 
ORDER BY left ASC;

如 E 的 left=5, right=6,其祖先节点为 B 和 A。


6. 查询上级路径并排好顺序

通过按 left 值升序排列即可还原路径:

SELECT * FROM dept 
WHERE left < 当前节点.left AND right > 当前节点.right 
ORDER BY left;

六、基于 left/right 的新增节点操作

新增节点,需要调整后续所有节点的编号:

场景:在部门 F(left=7, right=8)下新增 I 节点

步骤如下:

  1. 所有 left > 8 的节点:left + 2
  2. 所有 right >= 8 的节点:right + 2
  3. 插入 I 节点,left=8, right=9
  4. F 的 right 变成 10,树结构整体也向后偏移。

SQL 示例:

UPDATE dept SET right = right + 2 WHERE right >= 8;
UPDATE dept SET left = left + 2 WHERE left > 8;

INSERT INTO dept (name, left, right, parent_id) VALUES ('I', 8, 9, F_id);

七、删除节点操作与新增反向处理

删除节点也是类似:

  • 删除范围:left >= 当前.left AND right <= 当前.right
  • 所有 left > 当前.right:left - 节点宽度
  • 所有 right > 当前.right:right - 节点宽度

节点宽度为:right - left + 1


八、预排序模型的优点总结

  • 全树遍历快:只需按 left 升序排列即可;
  • 子树查询极快:通过 BETWEEN 查询一次性查出所有子孙节点;
  • 支持快速统计:通过 left/right 差值计算子节点数量;
  • 层级明确:配合 level 字段快速查找直属节点、祖先等。

九、缺点与优化策略

缺点:

  • 新增/删除开销大:left 和 right 涉及大量更新;
  • 不适合频繁变动的结构:如人员组织、实时关系树等;
  • 结构失衡后难维护:特别是嵌套层级多,频繁改动场景。

十、两种优化方案

1. 批处理更新

  • 利用定时任务或触发器,定期(如每5分钟)统一重建 left/right;
  • 对于非实时结构非常有效,避免每次插入都修改大量数据。

2. 提升数据库更新性能

  • 使用 SSD 硬盘、提升数据库写入速度;
  • 优化数据库参数:如 innodb_buffer_pool_size, innodb_log_file_size;
  • 批量更新代替逐条 update。

十一、适用与不适用场景对比

适用场景不适用场景
组织架构、人事结构、菜单结构实时多级分销、社交关系链、人员进出频繁
行政区划、产品分类结构频繁调整结构的评论树、消息树等

十二、使用建议总结

  • 若以从上至下查询为主,建议使用预排序模型(left/right);
  • 若以从下向上查找为主(如找所有祖先),建议使用 parent_id、path;
  • 二者可混合使用:用 parent_id 实时建树,用 left/right 存快照,定期更新;
  • 可结合缓存:使用 Redis 缓存整棵树结构或树路径,减少 DB 查询压力。

结语

通过为树形结构增加 left/right 编号,我们可以在 MySQL 中实现高效的树结构查询与统计。这种方案在处理部门组织、行政区划、权限菜单等“读多写少”的树形结构时,表现尤为优秀。但也要注意它在频繁结构变更下的开销问题,合理选择使用场景与更新机制,才能真正将其效能发挥到极致。

掌握了这个算法,你就拥有了一把处理树结构的利器,未来不论是复杂查询、结构可视化,还是层级分析,都会变得得心应手。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐