MySQL 8.0 的 WITH RECURSIVE 是唯一原生优雅实现树形查询的方案,锚点必须命中真实记录,否则静默返回空;查子树用 INNER JOIN + parent_id = 上一轮 id,level 和路径需在递归部分动态更新,超深树须调大 max_recursion_depth。

MySQL 8.0 的 WITH RECURSIVE 是唯一能真正“优雅”实现树形查询的原生方案——它不是语法糖,而是把迭代逻辑交还给数据库执行器,避免 N+1、不依赖应用层拼接、也不用硬编码 JOIN 层数。
锚点查询必须命中真实记录,否则结果为空但不报错
这是最常被忽略的隐性陷阱。比如查部门 ID = 100 的子树,但表里根本没有这条记录:WHERE id = 100 在锚点语句中不匹配任何行,整个递归 CTE 就直接返回空结果集,不会提示“找不到根节点”。
- 务必在锚点语句里加
EXISTS校验或业务层兜底判断 - 如果想查“可能不存在”的节点并返回空结果,没问题;但如果误以为查到了却实际没数据,就容易引发前端空列表或权限绕过
- 测试时建议先单独执行锚点部分:
SELECT * FROM department WHERE id = 100,确认有数据再套递归
向下查子树用 INNER JOIN,向上查父路径用反向关联
方向错了,递归链会断掉或引入 NULL 行。关键看 JOIN 条件怎么写:
- 查子树(从根往下):锚点查根,递归部分用
ON 子表.parent_id = 上一轮.id,推荐INNER JOIN,避免 parent_id 为 NULL 或非法值污染结果 - 查父路径(从叶子往上):锚点查叶子,递归部分用
ON 父表.id = 上一轮.parent_id,即拿当前行的parent_id去找它的父记录 - 千万别在向上查时写成
ON 上一轮.id = 父表.parent_id——这会完全颠倒父子关系,结果不可预测
level 字段和路径拼接必须在递归部分更新
level 和 full_path 这类衍生字段不能只在锚点初始化一次,必须在递归 SELECT 中显式计算,否则所有行 level 都是 0,路径也不会累加。
- 层级计数:锚点设
0 AS level,递归部分写prev.level + 1 AS level - 路径拼接:锚点用
CAST(name AS CHAR(200)) AS full_path,递归部分用CONCAT(c.name, ' > ', prev.full_path)(向下查则反过来) - 注意字符串长度限制:
CAST或CONCAT超长会被截断,深层树建议用TEXT类型列或提前预估最大深度
max_recursion_depth 默认 1000,超深树要手动调大
MySQL 不会因为层级太深就自动终止并报错,而是默默截断——比如你有一棵 2000 层的组织架构树,SELECT 只返回前 1000 层,且无任何警告。
- 运行时临时调整:
SET SESSION max_recursion_depth = 2000 - 永久生效需改配置文件
my.cnf,加max_recursion_depth = 2000 - 这个参数影响所有递归 CTE,不是单条 SQL 可控的,线上环境修改前务必评估内存开销
真正难的从来不是写出第一条能跑的递归 SQL,而是确保它在数据异常(如环状引用、脏 parent_id、缺失根节点)时行为可控——这些边界情况不会出现在示例数据里,但一定会在线上某个凌晨三点爆发。


















