MySQL 8.0中WITH RECURSIVE必须显式声明列名及类型,否则因锚点与递归分支类型不一致(如INT与BIGINT、数值与字符串混用)会触发ERROR 1221或隐式截断;所有分支列数、顺序、类型须严格一致,推荐在AS后括号内明确定义,如tree_cte(id, name, parent_id, level, path)。

MySQL 8.0 中 WITH RECURSIVE 查询必须显式声明递归列类型
不声明列类型会导致 ERROR 1221 (HY000): Incorrect usage of UNION and ORDER BY 或更隐蔽的隐式转换错误。MySQL 不像 PostgreSQL 那样自动推导递归 CTE 的列类型,尤其当锚点(anchor)和递归(recursive)分支返回值类型不一致时(比如 id 是 BIGINT,但递归中拼接了字符串),会直接报错或截断数据。
正确做法是在 WITH RECURSIVE 后立即用括号声明字段名及类型:
WITH RECURSIVE tree_cte (id, name, parent_id, level, path) AS ( SELECT id, name, parent_id, 0 AS level, CAST(id AS CHAR(100)) AS path FROM categories WHERE parent_id IS NULL UNION ALL SELECT c.id, c.name, c.parent_id, t.level + 1, CONCAT(t.path, '->', c.id) FROM categories c INNER JOIN tree_cte t ON c.parent_id = t.id )
-
CAST(id AS CHAR(100))强制统一path列为字符串类型,避免后续CONCAT出错 - 所有分支 SELECT 的列数、顺序、类型必须严格一致;类型不匹配时优先按锚点查询的类型为准,递归分支需主动适配
- 如果表中
id是INT UNSIGNED,而你在递归中用了负数偏移或表达式,可能触发隐式有符号转换,导致意外结果
递归深度超限会静默截断,必须主动设 cte_max_recursion_depth
MySQL 默认递归上限是 1000 层(cte_max_recursion_depth=1000),超过后查询不报错,而是直接停止递归并返回已有结果——这极易造成数据丢失却无感知。
执行前务必检查并设置合理上限:
SET SESSION cte_max_recursion_depth = 2000;
- 该变量是 session 级,不能在 SQL 语句里直接设,必须单独执行
- 若树实际深度可能接近上限,建议在递归 CTE 中加入
level 条件双重防护 - 线上环境注意:全局设置需
SET GLOBAL,但普通用户通常无权限;应由 DBA 预置合理默认值
无法用 ORDER BY 直接对递归结果排序层级路径
想让结果按「树形缩进」或「同级字典序」展示?别在外部 SELECT * FROM tree_cte ORDER BY ... —— 这会打乱递归生成的自然深度优先顺序,且 ORDER BY 在 CTE 外层不能影响递归执行逻辑。
- 层级缩进显示靠的是
level字段和重复空格:REPEAT(' ', t.level) - 同级排序必须在递归 JOIN 时完成,例如在
INNER JOIN后加ORDER BY c.sort_order, c.name—— 但注意:MySQL 不允许在UNION ALL的递归分支中写ORDER BY,只能靠子查询预排序 - 稳妥做法:把递归结果查出后,用应用层按
path字符串排序(如ORDER BY path),它天然支持树形遍历顺序
关联父节点名称时别在递归中多次 JOIN 同一张表
常见需求:查出每个节点及其父节点名称(parent_name)。如果在递归 CTE 内部每次 JOIN categories 表取 parent.name,会导致性能雪崩——每层递归都触发一次全表扫描或索引查找。
- 正确方式:只在锚点查询中 JOIN 一次父信息(适用于根节点有固定父标识的场景)
- 更通用做法:先用非递归查询把整张表的
id → name映射缓存为派生表,再在 CTE 外层 LEFT JOIN:
WITH RECURSIVE tree_cte AS (...), name_map AS (SELECT id, name FROM categories) SELECT t.*, m.name AS parent_name FROM tree_cte t LEFT JOIN name_map m ON t.parent_id = m.id;
这样避免递归过程中的重复 IO,也绕开了 MySQL 对 CTE 内部多表 JOIN 的优化限制。
真正麻烦的不是语法,而是递归路径字符串长度、字符集编码(比如 utf8mb4 下 CONCAT 可能触发隐式转换)、以及没有唯一索引的 parent_id 字段带来的笛卡尔积风险——这些细节不提前验证,上线后才暴露。











