MySQL 8.0+ 的 WITH RECURSIVE 是最直接可控的树形查询方式,需配合索引与防环逻辑:向下查子树时锚点选自身、JOIN 条件为 c.parent_id = s.id;向上查路径时锚点选叶子节点、CONCAT 前置拼接,并显式限制 depth 防环。

MySQL 8.0+ 的 WITH RECURSIVE 是目前最直接、可控的树形查询方式,但必须配合合理索引与路径构建逻辑,否则容易在深度 > 10 或宽度过大的树上触发性能雪崩。
用 WITH RECURSIVE 查子树(向下遍历)
这是最常见需求:给定一个节点 ID,查它所有后代(含自身)。关键在于初始查询选对起点,递归条件别写反。
- 初始查询必须是目标节点本身,不是它的子节点 —— 错误写法:
WHERE parent_id = ?(这查的是直接子节点,漏了自己) - 递归 JOIN 条件必须是
cte.id = t.parent_id(当前结果集的 id 去匹配下一层的 parent_id),写成t.id = cte.parent_id就会向上查祖先 - 如果表有百万级数据但树深度仅 3–5 层,
parent_id字段必须建索引,否则递归每轮都全表扫描
示例(查 ID=5 节点及其全部子孙):
WITH RECURSIVE subtree AS ( SELECT id, name, parent_id, 0 AS depth FROM categories WHERE id = 5 UNION ALL SELECT c.id, c.name, c.parent_id, s.depth + 1 FROM categories c INNER JOIN subtree s ON c.parent_id = s.id ) SELECT * FROM subtree ORDER BY depth, id;
用 WITH RECURSIVE 查完整路径(向上遍历)
查 “Laptops” 的路径 Electronics > Computers > Laptops,本质是向上找父节点并拼接字符串。难点在字符串拼接顺序和终止条件。
- 初始查询必须是叶子节点(如
WHERE id = 3),不能是根或中间节点,否则路径不完整 -
CONCAT要把新父节点名放在前面(CONCAT(p.name, ' > ', path)),否则拼出来是倒序 - 必须限制递归深度(加
depth < 20条件),防止环形引用(比如某条记录的parent_id指向自己)导致无限循环 -
CAST(path AS CHAR(1000))必须显式声明长度,否则 MySQL 可能截断或报错
示例:
WITH RECURSIVE path AS ( SELECT id, name, parent_id, CAST(name AS CHAR(1000)) AS full_path, 0 AS depth FROM categories WHERE id = 3 UNION ALL SELECT p.id, p.name, p.parent_id, CONCAT(p.name, ' > ', path.full_path), path.depth + 1 FROM categories p INNER JOIN path ON p.id = path.parent_id WHERE path.depth < 20 ) SELECT full_path FROM path WHERE parent_id IS NULL;
为什么不用闭包表或路径枚举?
路径枚举(如 path = '/1/5/3/')查子树确实快(WHERE path LIKE '/1/5/%'),但它在写操作时极脆弱:
- 移动节点需更新整棵子树的
path字段,事务开销大 - 并发修改同一父节点下的多个子节点,极易产生死锁
- 路径字符串长度难预估,
VARCHAR(255)在深度 > 15 时大概率不够用 - 无法用索引高效支持“查第 N 层所有节点”这类条件
除非你的业务是读多写少、树深固定、且能接受应用层强管控路径生成逻辑,否则邻接表 + WITH RECURSIVE 更实际。
MySQL 版本低于 8.0 怎么办?
没有 WITH RECURSIVE 时,硬编码 JOIN(如连 5 次 self-join)只适用于已知最大深度的场景,且维护成本高。更可行的方案是:
- 在应用层做迭代查询:先查出第一层子节点,再用这些 ID 批量查第二层,依此类推 —— 关键是每次用
IN传入 ID 列表,而非逐个SELECT - 引入临时表缓存中间结果,避免重复查询相同层级
- 若业务允许,把树结构预计算为扁平化视图(如每天凌晨跑一次存储过程生成
node_id, ancestor_id, depth表),用空间换时间
真正容易被忽略的点是:无论用哪种方式,只要树可能含环(即 A→B→C→A),就必须在应用层或 SQL 中加入防环机制 —— 仅靠外键约束无法阻止逻辑环,而环会导致递归查询卡死或返回错误结果。


















