
递归视图必须用 WITH RECURSIVE,且初始查询和递归查询要用 UNION ALL 连接
PostgreSQL、SQL Server(2017及以上)、SQLite(3.8.3+)以及标准SQL都强制要求显式声明 WITH RECURSIVE;MySQL 8.0+虽然也支持递归查询,但不强制写 RECURSIVE 关键字——不过加上它更安全,免得哪个版本行为不一致。漏掉这个关键字的结果很直接:数据库会报错,比如 ERROR: invalid reference to FROM-clause entry 或类似提示。
结构上来说,递归视图由两个部分组成:一个非递归的“锚点查询”(anchor),一个引用自身视图名的“递归查询”(recursive term)。这两部分必须通过 UNION ALL 连接起来——UNION 会去重,导致层级断裂;用逗号或 JOIN 直接拼则语法不合法。
CREATE VIEW org_tree AS WITH RECURSIVE tree AS ( -- 锚点:顶层节点(parent_id IS NULL 或 = 0) SELECT id, name, parent_id, 1 AS level FROM employees WHERE parent_id IS NULL UNION ALL -- 递归:关联到上一层的子节点 SELECT e.id, e.name, e.parent_id, t.level + 1 FROM employees e INNER JOIN tree t ON e.parent_id = t.id ) SELECT * FROM tree;
锚点查询必须能独立执行,且递归查询中只能引用一次视图名
锚点查询不能依赖递归视图本身,否则数据库无法启动迭代。一个常见的低级错误是把锚点写成 SELECT ... FROM tree WHERE ...,结果是报错 ERROR: recursive reference in anchor part。
递归查询中,视图名(比如上例的 tree)只能出现在 FROM 或 JOIN 子句里,并且只能出现一次;不能在 WHERE 中嵌套子查询引用自身,也不能多次 JOIN tree。多数数据库会直接拒绝(PostgreSQL 会报 ERROR: relation "tree" does not exist,其实是在解析阶段就拦截了)。
另外,锚点结果集的列名、类型、顺序必须和递归查询完全一致,否则类型不匹配会报错。比如锚点返回 TEXT,递归部分返回 VARCHAR(50),某些引擎试图隐式转换,结果可能失败。
必须设置迭代深度限制,否则可能死循环或超时
没有终止条件的递归——比如父子 ID 写成了环形——会让查询无限跑下去,直到触发数据库的默认限制。不同数据库的默认上限不一样:PostgreSQL 默认 max_recursion_depth = 100,SQL Server 是 100 层(可通过 OPTION (MAXRECURSION n) 调整),SQLite 是 1000(PRAGMA recursive_triggers 不影响此值)。
建议主动加防护:
- 在递归查询中加入
t.level < 50类似条件,避免意外深链拖垮性能。 - 对
parent_id字段建索引(CREATE INDEX idx_emp_parent ON employees(parent_id)),否则每次递归都要全表扫描。 - 如果业务允许,提前用
WITH RECURSIVE+LIMIT测试深度:SELECT * FROM org_tree ORDER BY level LIMIT 20。
不同数据库对 NULL 和根节点定义的处理差异很大
根节点怎么标?有的系统用 parent_id = 0,有的用 parent_id IS NULL,还有的用自引用(parent_id = id)。递归视图不会自动识别“根”,必须在锚点里明确写死逻辑。一旦写反,整个树就缺层。
更隐蔽的问题在 NULL 处理上:WHERE parent_id = t.id 在 t.id 为 NULL 时永远不成立(因为 NULL = NULL 的结果是 unknown),所以锚点若选了 parent_id IS NULL,递归部分就不能漏掉对 NULL 的防御,除非你确认数据里没有 NULL 父节点。
另外,SQL Server 要求递归列不能有聚合、窗口函数或外部引用;PostgreSQL 允许在递归分支中用 ORDER BY,但这个 ORDER BY 只影响该分支的输出顺序,不影响最终 UNION ALL 结果的行序——很多人误以为能靠它控制树遍历顺序,其实不行。
真正控制遍历顺序得靠外层查询加 ORDER BY,比如按路径字符串排序:SELECT * FROM org_tree ORDER BY lpad(level::text, 4, '0') || id。但路径字段得在递归过程中自己拼出来,不是白给的。这一点容易被忽略,值得专门记一下。
