必须写 WITH RECURSIVE,因为 MySQL 8.0+ 和 PostgreSQL 要求显式声明该关键字以启用递归功能,省略会导致语法错误,如 MySQL 报错 ERROR 1221;SQL Server 虽可省略,但为跨平台兼容性建议保留。

直接用 WITH RECURSIVE 就能实现多层级递归查询,但必须严格满足三要素:锚点(初始行)、递归引用、隐式终止;否则要么查不全,要么卡死或报错。
为什么必须写 WITH RECURSIVE 而不是普通 WITH
MySQL 8.0+ 和 PostgreSQL 要求显式声明 RECURSIVE 关键字,SQL Server 则不需要(但支持 WITH 后直接跟递归结构)。省略 RECURSIVE 会导致语法错误,例如 MySQL 报错:ERROR 1221 (HY000): Incorrect usage of WITH and RECURSIVE。PostgreSQL 同样拒绝非 RECURSIVE 声明下的自引用。
- MySQL / PostgreSQL:必须写
WITH RECURSIVE cte_name AS (...) - SQL Server:可省略
RECURSIVE,但建议保留以提高跨平台可读性 - SQLite 支持
RECURSIVE,且行为与 PostgreSQL 接近
UNION ALL 是强制要求,不能用 UNION、INTERSECT 或 EXCEPT
递归 CTE 的锚点和递归部分之间只能用 UNION ALL 连接。用 UNION 会触发去重逻辑,导致性能暴跌甚至无限循环——因为去重需要缓存全部中间结果,而递归过程本身是流式生成的。
- 锚点部分返回第 0 层数据(如顶级部门、根节点)
- 递归部分每次基于上一轮结果 JOIN 当前表,生成下一层
- 一旦某轮
JOIN没有匹配行,递归自动终止 - 若误用
UNION,数据库可能在每轮都尝试合并历史所有结果,内存爆满或超时
如何避免无限递归和栈溢出
最常见原因是父子关系字段未正确隔离,比如 ON t.parent_id = c.id 写成 ON t.id = c.id,导致每轮都返回相同行。默认递归深度限制因数据库而异:
- SQL Server 默认
MAXRECURSION 100,超限报错:The maximum recursion 100 has been exhausted - MySQL 无硬限制,但受
cte_max_recursion_depth系统变量控制(默认 1000) - PostgreSQL 通过
WITH RECURSIVE ... SELECT ... LIMIT n或应用层控制层数更安全 - 强烈建议在递归 JOIN 条件中加入层级计数和显式防环字段,例如:
AND t.id != c.id+c.level
实际层级查询中容易漏掉的字段设计
只查 ID 和 name 很难调试,生产环境务必包含:level(当前深度)、path(路径字符串,如 '1/5/12')、is_leaf(是否末级)。这些字段不光用于展示,更是排查循环的关键线索。
-
level可用1 AS level起始,递归中写c.level + 1 -
path推荐用字符串拼接:CAST(t.id AS TEXT)起始,递归中写c.path || '/' || t.id(PostgreSQL)或CONCAT(c.path, '/', t.id)(MySQL) - 如果原始表没维护
parent_id,而是用闭包表(closure table),那就别硬套递归 CTE——该用JOIN就用JOIN
真正麻烦的不是语法写不对,而是数据里存在自环(A 的 parent_id 是 A 自己)或双向引用(A→B 且 B→A)。这种脏数据会让任何递归 CTE 失效,得先清洗再查。

















