WITH RECURSIVE必须位于SQL语句最开头,作为顶层结构紧接主SELECT,不可嵌入子查询、视图或函数中;否则报错“syntax error at or near 'WITH'”。

WITH RECURSIVE 必须写在语句最前面,不能塞进子查询里
很多人想用嵌套查询“模拟”递归,结果在 FROM 或 WHERE 里硬塞 WITH RECURSIVE,直接报错:ERROR: syntax error at or near "WITH"。这不是语法写错了,是位置错了——WITH RECURSIVE 是整个查询的顶层结构,必须紧贴语句开头,后面紧跟主 SELECT,中间不能隔任何子句。
它不能出现在视图定义、函数体、或 INSERT ... SELECT 的子查询中;除非整个语句以 WITH RECURSIVE 开头。
实操建议:
- 先写
WITH RECURSIVE cte_name AS ( ... ),再换行写SELECT * FROM cte_name - 别试图把它当普通 CTE 嵌套:比如
SELECT * FROM (WITH RECURSIVE ...)是非法的 - MySQL 8.0+、PostgreSQL、SQLite 3.8.3+ 支持;SQL Server 用
WITH+UNION ALL(不加RECURSIVE关键字)
锚成员和递归成员必须同时存在,且用 UNION ALL 连接
漏掉锚成员(初始查询)是最常见错误。比如只写“找所有下属”,却没指定从哪个员工开始,数据库根本不知道从哪“启动”递归,会报错或返回空。
同样,如果递归成员里引用了锚成员没选的列(比如锚里只选 id, name,递归里却多加了 updated_at),会导致列数/类型不匹配,报错:column number/type mismatch in UNION ALL。
用 UNION 替代 UNION ALL 也危险:去重可能删掉关键中间结果,尤其当路径或层级有重复值时,迭代可能提前终止。
实操建议:
- 锚成员必须是独立
SELECT,不引用 CTE 自身 - 递归成员必须显式
JOIN或WHERE引用 CTE 名(如subordinates),不能靠隐式关联 - 两部分字段名、数量、类型必须严格一致;建议显式写出字段列表,别用
*
路径字符串匹配是旧版本 fallback 方案,不是递归替代品
MySQL 5.7 或更老版本不支持 WITH RECURSIVE,这时候有人用 LIKE 模拟层级,比如 c2.path LIKE CONCAT(c1.path, '%')。这看起来像递归,但本质是自连接+字符串扫描,性能差、易误匹配、不防环。
典型陷阱:路径存成 /1/5 和 /1/5/ 混用,/1/5% 就会错误命中 /1/50/;或者没加 c2.path != c1.path,导致每个节点统计都多算自己一次。
这种写法只适用于路径格式绝对统一、深度很浅、且无法升级数据库的场景。一旦数据量过千,响应明显变慢,且无法表达“第 N 层子节点”这类精确层级逻辑。
实操建议:
- 优先升级到支持
WITH RECURSIVE的版本(MySQL ≥ 8.0) - 若必须用路径匹配,确保所有
path以斜杠结尾(如/1/5/),并统一用c2.path LIKE CONCAT(c1.path, '%')+c2.path != c1.path - 对路径字段建前缀索引(如
INDEX(path(255)))能略微缓解性能问题
递归没有自动终止机制,环和深度失控得靠人来兜底
max_recursion_depth 不是保险丝。PostgreSQL 用 SET statement_timeout 或 WITH RECURSIVE ... LIMIT 配合 level 字段人工截断;MySQL 用 cte_max_recursion_depth 系统变量,但仅限会话级,且不检测数据环。
如果组织表里出现 A → B → C → A 这种闭环,或 parent_id 错误指向自身,查询就会卡死直到超时,甚至拖垮整个连接池。
实操建议:
- 在递归成员里显式加
WHERE level (或业务可接受的最大深度) - 用
ARRAY[...] || id(PostgreSQL)或字符串拼接记录已遍历路径,再用@visited变量(MySQL)做环检测——但这会显著增加复杂度 - 上线前务必用真实数据测环:把
parent_id故意设成子节点 ID,看是否报错或无限跑
真正难的不是写出第一版 WITH RECURSIVE,而是验证它在脏数据、环、边界深度下的行为是否可控。别依赖数据库自动兜底,每层 JOIN 条件、每个 level 计数、每次字符串拼接,都要当成可能断裂的链环来检查。










