递归CTE必须先定义锚点再写递归成员,顺序不可颠倒;需用UNION ALL连接,显式处理空值拼接路径,WHERE仅限锚点或最终结果过滤,防环需依赖数据库特性或手动实现。

递归 CTE 必须定义锚点和递归成员,且顺序不能颠倒
SQL Server、PostgreSQL(14+)、Oracle 都支持递归 CTE,但语法细节有差异。核心是两部分必须显式分开:WITH RECURSIVE(PostgreSQL)或 WITH(SQL Server),然后先写锚点查询(即最顶层节点),再用 UNION ALL 连接递归查询。如果把递归部分写在前面,或用了 UNION(去重),会直接报错或结果异常。
常见错误现象:Maximum recursion exceeded(SQL Server)、recursive reference must be in FROM clause(PostgreSQL)。这是因为递归调用没正确引用自身别名,或没限制层级深度。
- 锚点部分只能查出根节点(例如
parent_id IS NULL或level = 1) - 递归部分的
FROM必须包含 CTE 自身的别名(如FROM tree t JOIN cte ON t.parent_id = cte.id) - SQL Server 默认递归上限 100 层,超限需加
OPTION (MAXRECURSION n),设为 0 表示无限制(慎用)
拼接全路径字符串要用字符串聚合,注意空值和分隔符位置
层级路径本质是把祖先节点名按从顶到底顺序连起来,比如 'A/B/C'。不能依赖递归中的简单 + 或 || 拼接,因为初始锚点没有父路径,必须显式处理空值;否则会出现 NULL/B/C 或整个字段变 NULL。
不同数据库处理方式不同:
- SQL Server:用
ISNULL(cte.path, '') + '/' + t.name,锚点中 path 初始化为t.name - PostgreSQL:用
COALESCE(cte.path || '/', '') || t.name,更安全;锚点中 path 设为t.name::TEXT - Oracle:用
COALESCE(cte.path || '/', '') || t.name,但需确保字段类型一致(避免隐式转换失败)
如果路径含斜杠、反斜杠等特殊字符,建议提前 REPLACE 转义,否则后续解析困难。
WHERE 条件不能下推到递归成员内部,否则会截断树形结构
有人想“只查某个子树”,于是把 WHERE t.id = 123 写在递归查询里 —— 这会导致只返回该节点自身,无法向上追溯或向下展开。正确做法是:锚点定位根(或指定起点),递归部分保持开放连接,最后在外部 SELECT 中过滤。
例如要获取 ID=123 的完整向上路径(直到根),锚点应查 WHERE id = 123,递归部分用 JOIN ... ON t.id = cte.parent_id(反向向上);若要查它所有后代,则锚点查它自己,递归部分用 ON t.parent_id = cte.id(正向向下)。
- 递归 CTE 的 WHERE 只能用于锚点或最终结果集,不能出现在递归分支的
FROM子句之后 - 需要双向路径(如既向上又向下),得写两个 CTE 分别处理,再
UNION ALL - 性能敏感场景,确保
parent_id和id字段都有索引
PostgreSQL 14+ 支持 SEARCH 和 CYCLE,SQL Server 需手动防环
真实数据常有脏数据导致循环引用(如 A→B→C→A),递归 CTE 会无限执行直至超时或报错。PostgreSQL 14 引入 SEARCH DEPTH FIRST BY id SET ordercol 和 CYCLE 子句自动标记环路;SQL Server 没原生支持,必须靠路径字符串检测重复 ID 或用临时表记录已访问节点。
手动防环示例(SQL Server):
锚点中初始化 CAST(',' + CAST(t.id AS VARCHAR) + ',' AS VARCHAR(800)) AS path_ids,递归中拼接时检查 CHARINDEX(',' + CAST(t.id AS VARCHAR) + ',', cte.path_ids) = 0,再追加。
这个逻辑容易漏掉边界情况:ID 是字符串、含前导零、或路径过长被截断。生产环境建议优先清理数据,而非靠 SQL 层兜底。










