递归CTE必须包含锚点和递归成员两部分,锚点查顶层节点(如parent_id IS NULL),递归成员通过JOIN自身实现“从父找子”,且需手动维护level字段并设深度限制以防无限循环。

递归 CTE 必须包含两个部分:锚点和递归成员
缺一不可,否则报错 Recursive common table expression '。锚点查顶层节点(如 parent_id IS NULL 或 level = 0),递归成员用 JOIN 自身,且必须引用 CTE 名字本身 —— 这是 SQL 标准强制要求,不是可选项。
常见错误是把递归条件写成 ON t.parent_id = cte.id(反了),正确方向是「从父找子」:ON t.parent_id = cte.id 表示当前层的子节点,cte 是上一层结果。
- 锚点里不能用聚合、GROUP BY、ORDER BY(除 TOP/NEXT 在 SQL Server/PostgreSQL 中有限支持)
- 递归成员中不能出现 LEFT JOIN(多数数据库不支持,会报错或静默截断)
- PostgreSQL 要求递归分支必须在
UNION ALL左侧,SQL Server 和 Oracle 类似;MySQL 8.0+ 同样严格
控制递归深度防止无限循环
树形结构若存在脏数据(如 A → B → A 循环引用),递归 CTE 会一直跑直到超时或达到数据库默认限制。必须显式设上限:
- PostgreSQL:在
WITH RECURSIVE后加SEARCH DEPTH FIRST BY id SET path,再用WHERE depth - SQL Server:用
OPTION (MAXRECURSION n),例如OPTION (MAXRECURSION 32)(默认 100,0 表示无限制,慎用) - MySQL:没有原生深度控制,靠
level 在递归分支 WHERE 中硬过滤
注意 level 字段要手动维护:锚点设为 0 或 1,递归中写 cte.level + 1。别漏掉它,否则无法限深也无法排序层级。
按树形顺序排序(同层兄弟有序、整体深度优先)
单纯 ORDER BY level, name 只能保证先浅后深、同层字典序,但无法让「A 的子树全部排在 A 后面」—— 即真正的树遍历序。需要构造路径字段:
- PostgreSQL:用
SEARCH子句自动生成path数组,直接ORDER BY path - SQL Server / MySQL:手动拼
CONCAT(cte.path, '/', t.id),锚点初始化为CAST(t.id AS VARCHAR(500)),递归中拼接 - 避免用
id字符串拼接(如'1/12/125'),因为排序会变成字典序('10'
如果只要展开平级结构(比如只显示直属下级),就不需要路径,ORDER BY parent_id, sort_order 更高效。
实际建模中容易被忽略的细节
部门表或评论表往往有软删除(is_deleted = true)、状态字段(status = 'active')、租户隔离(tenant_id = ?)。这些条件不能只加在锚点里 —— 递归成员也必须重复过滤,否则可能拉出已删节点的子树。
另外,SELECT * 在递归 CTE 中极危险:不同层级字段数/类型必须一致,而某些数据库对 NULL 处理敏感(如 MySQL 中 INT 和 NULL 混合可能导致隐式转换失败)。始终显式列出字段,并用 CAST(NULL AS ...) 对齐空值类型。
真实场景里,父 ID 为空不等于就是根节点 —— 有些系统用特殊值(如 -1 或 0),有些允许多根。确认业务语义比套模板更重要。










