递归cte必须用with recursive,漏写recursive会报错;必须用union all连接锚点与递归成员;锚点须返回至少一行;递归引用名须严格一致;默认递归深度上限1000;parent_id等关联字段必须建索引;向上/向下递归join条件方向相反;路径拼接需统一类型并预估长度。

递归CTE必须用 WITH RECURSIVE,不是 WITH
MySQL 8.0+ 不支持普通 WITH 做递归,漏写 RECURSIVE 关键字会直接报错 ERROR 1248: Every derived table must have its own alias 或更隐蔽的语法错误。这是新手最常卡住的第一步。
锚点和递归成员之间必须用 UNION ALL,不能用 UNION:后者去重会意外截断同名节点(比如两个同名子部门),导致递归提前终止、结果不全。
- 锚点部分必须能返回至少一行,否则整个递归不执行(比如
WHERE id = 999查不到,subordinatesCTE 就为空) - 递归成员中引用自身时,表别名要一致——
JOIN subordinates s里的subordinates必须和 CTE 名完全相同,大小写敏感 - 递归深度默认上限是 1000,超深树会报错
ERROR 3636: Recursive query aborted after 1000 iterations,需临时调大:SET SESSION cte_max_recursion_depth = 3000;
parent_id 字段必须有索引,否则性能断崖式下跌
递归每轮都靠 JOIN 当前结果集去查下一层,本质是 N 次等值查询。如果 parent_id 没索引,每次都要全表扫描,10 层深度可能触发百万级行扫描。
建索引不是可选项,是必选项:
CREATE INDEX idx_parent_id ON categories(parent_id);- 若常按
id查起点,主键已覆盖,无需额外操作;否则补INDEX(id) - 避免在递归字段上用函数,比如
ON c.id = CAST(p.parent_id AS SIGNED)会让索引失效
向上递归(找父级)和向下递归(找子级)的 JOIN 条件刚好相反
向下查子树(如查某部门所有下属):递归时用 e.manager_id = s.id,即“子表 manager_id 等于上一轮的 id”。
向上查祖先(如生成面包屑路径):递归时用 c.id = p.parent_id,即“原表 id 等于上一轮的 parent_id”——方向反了,写错就查成兄弟节点或空结果。
- 向下场景示例:
SELECT e.* FROM employees e JOIN subordinates s ON e.manager_id = s.id - 向上场景示例:
SELECT c.* FROM categories c JOIN path_cte p ON c.id = p.parent_id - 混淆后典型现象:结果行数极少、
depth停在 1、路径拼接只有一级
字符串路径拼接要用 CAST 初始化类型,否则报错
递归 CTE 中所有列类型必须严格一致。full_path 在锚点是 VARCHAR,递归里用 CONCAT 拼接时若没显式转类型,MySQL 可能推导为 BLOB 或超长 TEXT,导致 UNION ALL 类型冲突,报错 ERROR 1270: Illegal mix of collations 或 ERROR 1052: Column 'xxx' in field list is ambiguous。
- 锚点必须初始化:
CAST(name AS CHAR(500)) AS full_path - 递归里保持同宽:
CONCAT(c.name, ' / ', p.full_path)(不用再 CAST) - 长度预估要留余量,路径过长会被截断,且无法事后扩容
真正难的不是写出第一版递归SQL,而是确保它在数据量涨10倍、层级深2层、并发增3倍时仍不崩——索引、类型、终止条件这三处,漏一个就从“一次查完”退化成“应用层循环查”。











