递归CTE必须用UNION ALL,因语法强制且去重会破坏迭代链式传递、导致收敛失效、性能下降和语义丢失;UNION在多数数据库中直接报错,且无法保证递归正确性。

必须用 UNION ALL,不能用 UNION —— 否则递归会中断、报错或漏数据。
为什么递归CTE里UNION ALL不可替换
递归执行依赖“上一轮结果 → 下一轮输入”的链式传递,UNION 的隐式去重会破坏这个过程:一旦某层出现重复 id(比如多个子节点指向同一父节点),UNION 就直接砍掉冗余行,导致后续层级丢失。更严重的是,SQL Server 会直接报 Recursive common table expression 'tree' does not contain a recursive reference,PostgreSQL 则提示 recursive reference must be in rightmost term of UNION。
常见错误写法:
WITH RECURSIVE tree AS ( SELECT id, name, parent_id, 0 AS level FROM orgs WHERE id = 123 UNION -- ❌ 错!这里必须是 UNION ALL SELECT o.id, o.name, o.parent_id, t.level + 1 FROM orgs o JOIN tree t ON o.parent_id = t.id )
-
UNION ALL保证每次迭代结果无损叠加,是递归成立的前提 - 性能上,
UNION ALL比UNION快一个数量级,因跳过排序和去重 - 即使你确定数据无重复,也别冒险——数据库优化器不保证锚点与递归部分字段顺序一致,
UNION可能意外触发去重逻辑
JOIN方向必须是“子表 → 递归CTE”,不能反着写
递归CTE的别名(如 tree)代表“已查出的上层节点”,要找它的下级,就得让原始表(orgs)通过 parent_id 去匹配 tree.id。写反了(比如 tree.parent_id = orgs.id)会导致查出的是“上级”,而非“下级”,整个树倒挂。
正确写法示例(查某部门所有子孙):
WITH RECURSIVE tree AS ( SELECT id, name, parent_id, 0 AS level FROM orgs WHERE id = 123 UNION ALL SELECT o.id, o.name, o.parent_id, t.level + 1 FROM orgs o JOIN tree t ON o.parent_id = t.id -- ✅ 正确:子表的 parent_id 指向上层节点 )
- JOIN 条件中,左侧必须是基础表(
orgs),右侧必须是 CTE 别名(tree) - 如果业务需要向上查祖辈(如员工→部门→事业部→集团),则条件改为
t.parent_id = o.id,但此时锚点应为叶子节点,且需确保parent_id不为空 - 别名命名要有语义,比如叫
ancestors或descendants,避免混用
路径查询必须用 LIKE 或正则,不能用 =
当你用字符串拼接生成路径(如 '/1/5/23'),想查“节点 5 下所有后代”,直接 WHERE path = '/1/5' 是查不到任何结果的——因为完整路径都带尾部斜杠或更深层级。真正要匹配的是前缀。
不同数据库推荐写法:
- 通用安全写法:
WHERE path LIKE '/1/5/%'(开头加/,结尾加/%) - PostgreSQL 更精确:
WHERE path ~ '^/1/5(/|$)',防止/1/50被误匹配 - SQL Server 若用
LEFT(path, LEN('/1/5/') ) = '/1/5/',需注意path字段是否以/开头且结尾无/
容易踩的坑:前端传入 parent_path = '/1/5',后端直接拼成 WHERE path = :parent_path,结果永远为空。必须主动补 % 或改用正则。
深度限制必须写在递归分支内部,不能只靠 OPTION (MAXRECURSION)
OPTION (MAXRECURSION N)(SQL Server)或全局 max_recursion_depth(MySQL)只是最后防线,无法替代逻辑层控制。真正的限制必须放在递归查询的 WHERE 子句里,否则一旦存在环(如 A→B→A),数据库会在第 N+1 层才终止,已产生大量无效中间结果,浪费内存与时间。
正确做法:
WITH RECURSIVE tree AS ( SELECT id, name, parent_id, 0 AS level FROM orgs WHERE id = 123 UNION ALL SELECT o.id, o.name, o.parent_id, t.level + 1 FROM orgs o JOIN tree t ON o.parent_id = t.id WHERE t.level < 4 -- ✅ 控制最多展开到第 4 层(即根+4层子) )
- 条件写在递归部分的
WHERE,不是锚点部分,也不是外层SELECT - PostgreSQL 不支持
OPTION,必须靠t.level ;MySQL 8.0 需同时设 <code>cte_max_recursion_depth和逻辑限制 - 如果 JOIN 外层表(如
JOIN users u ON u.dept_id = tree.id),这个限制不会自动继承,仍需在 CTE 内完成
路径格式统一、JOIN 方向、UNION ALL 强制性、深度条件位置——这四点只要错一个,递归就可能静默失败或返回错误结果,比语法报错更难排查。

















