MySQL 8.0递归CTE需严格满足三要素:必须写RECURSIVE关键字、锚与递归成员间用UNION ALL连接、递归部分禁用ORDER BY/LIMIT/聚合等;JOIN方向决定上下级关系,parent_id字段须建索引,深度超限需调大cte_max_recursion_depth。

MySQL 8.0 的递归 CTE 能一次性查出任意深度的树形数据,但必须写对结构、加好索引、设好终止条件,否则直接报错或查不出结果。
为什么 WITH RECURSIVE 会报 ERROR 1221 或拒绝执行
最常见原因是语法踩了硬性限制:漏写 RECURSIVE 关键字、用 UNION 代替 UNION ALL、递归部分写了 ORDER BY 或 LIMIT。MySQL 8.0 对递归 CTE 的解析非常严格,锚成员和递归成员之间必须用 UNION ALL 连接,且递归查询里不能出现聚合、窗口函数、DISTINCT。
-
WITH tree AS (...)→ 缺少RECURSIVE,必定报错 -
SELECT ... UNION SELECT ...→ 不是UNION ALL,优化器直接拒绝 - 递归分支里写了
ORDER BY id或WHERE level 放在 <code>SELECT后 → 语法错误
WITH RECURSIVE 的锚成员和递归成员怎么写才不循环
锚成员是起点,必须能独立执行;递归成员是“用上一轮结果去查下一轮”,关键在于 JOIN 条件方向不能反。比如查子节点,就要让子表的 parent_id 去匹配 CTE 的 id;如果反过来写成 cte.parent_id = t.id,就会查上级而非下级,还可能因 NULL 导致空结果。
- 锚成员示例:
SELECT id, name, parent_id FROM categories WHERE parent_id IS NULL(查顶层) - 递归成员正确写法:
SELECT c.id, c.name, c.parent_id FROM categories c INNER JOIN tree t ON c.parent_id = t.id - 错误写法:
... ON t.parent_id = c.id(逻辑翻转,查的是父辈) - 安全终止:靠数据自然断链(如最后一层没子节点),不是靠
WHERE控制轮次
查深层级时性能突然变慢甚至超时,怎么办
递归 CTE 是迭代执行的:第 1 轮跑锚查询,第 2 轮拿第 1 轮结果 JOIN,第 3 轮拿第 2 轮结果 JOIN……每轮都走一次索引查找。如果 parent_id 没索引,每轮都是全表扫描,10 层就扫 10 次全表。
- 必须为外键字段建索引:
CREATE INDEX idx_parent_id ON categories(parent_id) - 主键
id默认有索引,不用额外处理 - 深度超过 1000 会截断(默认
cte_max_recursion_depth=1000),需手动调大:SET SESSION cte_max_recursion_depth = 3000 - 避免在递归分支里做字符串拼接(如
CONCAT路径),它会在每轮重复计算,拖慢迭代速度
查某节点的所有祖先(向上递归)和所有后代(向下递归)的区别
方向决定 JOIN 条件和锚点选择。向下查后代,锚点是起点节点,递归时用子表 parent_id = cte.id;向上查祖先,锚点还是起点节点,但递归时要用父表 id = cte.manager_id,也就是拿当前行的 manager_id 去找它的直属上级。
- 查下属(向下):
ON e.manager_id = s.id(员工的经理 ID 等于上层员工 ID) - 查上级(向上):
ON m.id = s.manager_id(经理表的 ID 等于当前员工的 manager_id) - 两种写法不能混用,否则结果为空或逻辑错乱
- 向上递归更容易遇到 NULL 终止(CEO 的
manager_id是 NULL),所以通常更快结束
真正难的不是写出第一版递归 SQL,而是确认 JOIN 方向是否符合业务意图、索引是否覆盖到每次迭代的查找字段、以及递归深度是否在可控范围内——这三处任一出错,查询就既慢又错。











