MySQL 8.0+ 支持 WITH RECURSIVE 实现树形遍历,可正向查子树或反向查祖先路径;低版本需用应用层递归、闭包表或路径枚举替代,并须为 parent_id 建索引以保障性能。

MySQL 8.0+ 用 WITH RECURSIVE 实现树形遍历
MySQL 8.0 起原生支持递归 CTE,这是处理无限级分类最直接的方式。低于 8.0 的版本无法使用 WITH RECURSIVE,硬上会报错 ERROR 1064 (42000): You have an error in your SQL syntax。
假设分类表 category 结构为:id、name、parent_id(根节点 parent_id 为 NULL 或 0):
WITH RECURSIVE tree AS ( SELECT id, name, parent_id, 0 AS level FROM category WHERE parent_id IS NULL -- 根节点条件 UNION ALL SELECT c.id, c.name, c.parent_id, t.level + 1 FROM category c INNER JOIN tree t ON c.parent_id = t.id ) SELECT * FROM tree ORDER BY level, id;
- 必须显式声明递归字段类型一致(如
parent_id和t.id都是INT,否则可能触发隐式转换失败) -
UNION ALL比UNION快且必要——递归中不允许去重逻辑 - 递归深度默认受
cte_max_recursion_depth限制(默认 1000),超深树需提前设:SET SESSION cte_max_recursion_depth = 3000;
查询某节点的所有祖先路径(向上递归)
不是所有场景都要查子树;比如点击商品分类时,需展示“手机 > 智能手机 > Android 手机”这种面包屑路径,就得从叶子节点往根回溯。
关键点在于起始条件改为叶子节点,JOIN 条件反向:
WITH RECURSIVE ancestors AS ( SELECT id, name, parent_id, 0 AS depth FROM category WHERE id = 123 -- 目标子节点 ID UNION ALL SELECT c.id, c.name, c.parent_id, a.depth + 1 FROM category c INNER JOIN ancestors a ON c.id = a.parent_id -- 注意这里是 c.id = a.parent_id ) SELECT * FROM ancestors ORDER BY depth DESC;
- 如果
parent_id允许为0表示根,则终止条件需额外过滤:WHERE c.parent_id != 0,否则会多一层无效记录 - 结果顺序靠
ORDER BY depth DESC保证根在前,但实际应用中常配合GROUP_CONCAT(... ORDER BY depth DESC SEPARATOR ' > ')直接拼路径
低版本 MySQL(5.7 及以下)只能靠应用层或存储过程模拟
没有 WITH RECURSIVE 时,SQL 层无法真正递归。常见替代方案效果和维护性都差:
- 多次查询:先查一级子类,再对每个 ID 查二级……容易 N+1,且深度不确定时代码难写死
- 闭包表(closure table):额外建
category_closure表存所有祖先-后代关系对,插入/更新需同步维护,适合读远多于写的场景 - 路径枚举(path enumeration):在
category表加path字段存类似'/1/5/12/'的字符串,用LIKE '/1/5/%'查子树——但path更新成本高,且无法用索引高效优化
如果必须用老版本又不想改表结构,建议把树拉到应用内存里(如 PHP/Python 中构建嵌套数组),用 DFS/BFS 一次性处理,比反复查库更稳。
性能与索引注意事项
递归查询性能不只取决于语法,更依赖基础索引。没索引的 parent_id 字段会让每次 JOIN 变全表扫描,树一深就卡死。
- 必须为
parent_id建索引:CREATE INDEX idx_parent_id ON category(parent_id); - 若常按名称查节点再展开,可考虑联合索引:
CREATE INDEX idx_parent_name ON category(parent_id, name); - 递归结果集大时,
ORDER BY会触发 filesort;如只需子树列表不关心顺序,删掉ORDER BY能明显提速
真实业务里,树形结构往往还带状态字段(如 is_active),记得在递归 CTE 的两个分支里都加上对应 WHERE 条件,否则可能把已下架分类也拉出来。


















