递归CTE必须包含锚点和递归成员两部分,缺一不可;SQL Server、PostgreSQL、SQLite均支持,但PostgreSQL/SQLite需WITH RECURSIVE,SQL Server可省略;二者均须用UNION ALL连接,且字段数量、名称、类型严格一致,否则报错。

递归CTE必须包含锚点和递归成员两部分
SQL Server、PostgreSQL、SQLite(3.8.3+)支持递归CTE,但语法稍有差异。核心是:WITH RECURSIVE(PostgreSQL/SQLite)或WITH(SQL Server),且必须由两个用UNION ALL连接的查询组成:锚点(初始行)和递归成员(引用自身CTE名)。漏掉任一部分会报错,比如 PostgreSQL 报 recursive reference to query "xxx" must be in a UNION,SQL Server 报 The statement terminated. The maximum recursion 100 has been exhausted(其实是没写对结构,不是深度问题)。
常见错误是把条件全塞进递归部分,导致无限循环或无结果。锚点应只查顶层节点(如 parent_id IS NULL 或 level = 0),递归部分才做自关联。
- 锚点查询不能依赖递归CTE别名,否则语法错误
- 递归成员中,CTE别名只能出现在
FROM子句,不能在WHERE里直接用别名字段做过滤(需用JOIN或子查询) - SQL Server 默认递归深度上限为100,超限需加
OPTION (MAXRECURSION n),设为0表示无限制(慎用)
PostgreSQL 和 SQL Server 的递归语法差异
PostgreSQL 强制要求WITH RECURSIVE关键字;SQL Server 允许省略RECURSIVE,但语义相同。字段别名定义位置也不同:PostgreSQL 要求在AS后括号内声明列名,SQL Server 可在CTE定义里或内部查询中指定。
示例:查组织架构树(表org含id、name、parent_id):
-- PostgreSQL WITH RECURSIVE tree(id, name, parent_id, level) AS ( SELECT id, name, parent_id, 0 FROM org WHERE parent_id IS NULL UNION ALL SELECT o.id, o.name, o.parent_id, t.level + 1 FROM org o JOIN tree t ON o.parent_id = t.id ) SELECT * FROM tree ORDER BY level, id;
-- SQL Server WITH tree AS ( SELECT id, name, parent_id, 0 AS level FROM org WHERE parent_id IS NULL UNION ALL SELECT o.id, o.name, o.parent_id, t.level + 1 FROM org o INNER JOIN tree t ON o.parent_id = t.id ) SELECT * FROM tree OPTION (MAXRECURSION 500);
- PostgreSQL 不支持
OPTION子句,深度控制靠SET statement_timeout或应用层截断 - SQL Server 中
UNION ALL不可换成UNION,否则报错:递归CTE不允许去重 - 字段类型必须严格一致,比如
level在锚点和递归部分都得是INT,否则 PostgreSQL 会提示column "level" has type integer but expression has type numeric
避免无限循环的关键:确保递归条件收敛
递归不会自动终止,必须靠连接条件天然形成“向下一层”的路径。如果parent_id指向自身(如id = parent_id)、或存在环(A→B→C→A),查询会卡死或超限报错。
安全做法是在递归部分加入层级限制或路径记录:
- 加
level < 10硬限制(适用于已知最大深度的场景) - PostgreSQL 可用数组记录访问路径:
ARRAY[id]锚点初始化,递归中用t.path || o.id拼接,再用o.id = ANY(t.path)检测环 - SQL Server 没原生路径函数,可用
CHARINDEX拼接字符串路径(如'/' + CAST(t.id AS VARCHAR) + '/'),但性能差,仅作兜底
实际业务中,建议在写入时校验环(触发器或应用层),而非依赖查询时防御。
性能差?先确认是否真需要递归CTE
递归CTE 是逐层展开,时间复杂度接近 O(N²),尤其当树宽大、深浅不均时。如果只是查某节点的所有父级(向上追溯),用非递归方式往往更快:比如在org表上建parent_id索引,然后用程序循环查(3–4次SQL即可);或者冗余存储path字段(如/1/5/23/),用LIKE匹配子树。
- CTE 无法被物化(除 PostgreSQL 12+ 的
MATERIALIZED提示外),每次执行都重算 - SQL Server 对递归结果不走索引,即使
ORDER BY字段有索引,排序仍发生在内存中 - 如果只需叶子节点,别在递归里
WHERE过滤——应在外层SELECT中筛,否则剪枝失效
真正绕不开递归的场景其实不多:动态深度的权限继承、多级分销关系、BOM 展开。其它情况,先画出数据访问模式,再决定要不要扛住递归的代价。

















