mysql 8.0+递归cte必须显式使用with recursive,锚点与递归部分须用union all连接,递归查询禁用group by、order by等,列名数量类型顺序须严格一致,终止依赖递归分支where条件控制层级或防环。

递归CTE必须显式加RECURSIVE关键字
MySQL 8.0+ 不接受 WITH cte_name AS (...) 写法来定义递归查询——哪怕里面用了自引用,也会报错 Recursive reference not allowed。必须写成 WITH RECURSIVE cte_name AS (...),否则解析器直接拒绝执行。
这个限制不是可选提示,是硬性语法校验。漏掉 RECURSIVE,哪怕逻辑完全正确,也会在准备阶段失败。
- 锚点查询(非递归部分)和递归查询之间只能用
UNION ALL,不能用UNION(去重会破坏迭代一致性) - 递归查询中不能出现
GROUP BY、ORDER BY、LIMIT、DISTINCT或窗口函数,否则报错Recursive member cannot contain ... - 列名必须对齐:锚点和递归部分的 SELECT 字段数、类型、顺序要严格一致;建议显式声明列名列表,比如
WITH RECURSIVE tree (id, name, level) AS (...)
终止条件靠WHERE控制,不是靠行数
递归不会因为“查到第100行就停”,而是按轮次(iteration)执行:第0轮跑锚点,第1轮用第0轮结果驱动JOIN,第2轮用第1轮新增行再驱动……直到某一轮递归查询返回空结果集,才停止。
所以 WHERE 必须写在递归部分的 SELECT 里,且要能真正过滤掉后续无效路径。常见错误是只写 WHERE parent_id IS NOT NULL,但没限制层级或防环,导致无限递归或超深报错 Recursive query aborted after 1000 iterations。
- 安全做法是引入
level字段并加WHERE level 类约束 - 防环更可靠的方式是构建路径字符串(如
CONCAT(path, '/', id)),再用NOT LIKE CONCAT('%/', id, '/%')检查是否已存在 - MySQL 默认最大轮次为 1000,可通过
SET SESSION cte_max_recursion_depth = 2000临时调高,但治标不治本
递归部分的JOIN必须把CTE放右边
MySQL 要求递归成员中,CTE 必须作为被驱动表出现在 JOIN 右侧,即形如 FROM departments d JOIN dept_tree dt ON d.parent_id = dt.id。如果写成 FROM dept_tree dt JOIN departments d ON d.parent_id = dt.id,会报错 Recursive reference in FROM clause is not allowed。
这不是风格问题,是执行引擎的硬性设计:每轮迭代以上轮 CTE 输出为驱动源,去关联基础表。反过来逻辑不通。
- 别试图用
LEFT JOIN或RIGHT JOIN绕过——只支持INNER JOIN或隐式JOIN - 连接字段(如
parent_id)务必建索引,否则每轮都是全表扫描,性能断崖式下跌 - 如果基础表有大量孤儿节点(
parent_id指向不存在的id),它们会在每轮都参与JOIN却无匹配,白白消耗资源
聚合统计必须放在最终SELECT,不能塞进递归体
想统计每层节点数?别在递归部分写 GROUP BY level——这会直接触发语法错误。所有聚合、排序、去重操作,必须留在 SELECT * FROM cte_name 这一层做。
例如要算每个层级的子部门数量,得这样写:
WITH RECURSIVE dept_tree AS ( SELECT id, name, parent_id, 1 AS level FROM departments WHERE parent_id IS NULL UNION ALL SELECT d.id, d.name, d.parent_id, dt.level + 1 FROM departments d JOIN dept_tree dt ON d.parent_id = dt.id ) SELECT level, COUNT(*) AS count FROM dept_tree GROUP BY level;
递归体只负责“展开结构”,最终结果集才是你加工数据的地方。混淆这两层职责,是调试时最常卡住的点。
真实树形数据往往有深度不均、分支不一、路径交叉等特点,WITH RECURSIVE 提供的是结构展开能力,不是业务聚合引擎——把聚合逻辑塞进去,等于让发动机干变速箱的活。











