WITH RECURSIVE 递归查询必须显式设置终止条件(如 depth 限制或路径去重),否则 PostgreSQL/SQLite/MySQL 8.0+ 均报错;依赖数据自然结束不可靠,需用 ARRAY 类型配合 NOT … ANY(path) 防环并避免误匹配。

WITH RECURSIVE 递归查询必须有终止条件,否则直接报错
PostgreSQL 和 SQLite 支持 WITH RECURSIVE,但 MySQL 8.0+ 才支持;不加明确的终止逻辑会触发 ERROR: recursive query terminated due to cycle detection 或更常见的 ERROR: stack depth limit exceeded。这不是性能问题,是 SQL 引擎强制的安全限制。
实际写法里,终止靠的是递归成员中对 depth 或路径字符串长度的显式约束,而不是依赖“数据自然结束”。图里没有环 ≠ 查询不会无限递归——比如起点连向自己、或两个节点互相指向,都可能绕过环检测(尤其用字符串拼接路径时)。
- 必须在递归部分加入
WHERE depth 这类硬限制(根据业务场景调) - 推荐用数组记录已访问节点:
NOT node_id = ANY(path),比字符串NOT LIKE更可靠 - SQLite 对递归深度默认只允许 1000 层,需运行
PRAGMA recursive_triggers = ON并设PRAGMA max_recursive_depth = 2000
构建路径时优先用 ARRAY 而非字符串拼接
用 CONCAT(prev_path, '->', node_id) 看似直观,但会导致无法准确判断是否成环(比如 1->12 会被 '1' 错误匹配),也难做去重和长度统计。PostgreSQL 的 ARRAY 类型天然支持 UNION 去重、@> 包含判断、array_length() 计数。
典型错误是把初始 CTE 的 path 设为 ARRAY[node_id],却在递归部分写成 ARRAY_APPEND(path, next_node) ——这没问题,但若漏掉 DISTINCT ON (node_id),同一节点多次被不同路径抵达时,会重复展开子树,爆炸式增长结果行数。
- 初始查询:
SELECT id AS node_id, ARRAY[id] AS path, 0 AS depth FROM nodes WHERE id = 1 - 递归查询:
SELECT e.to_node, path || e.to_node, depth + 1 FROM edges e JOIN t ON e.from_node = t.node_id WHERE NOT e.to_node = ANY(path) AND depth - MySQL 不支持
ARRAY,只能退化用 JSON 数组或字符串,此时必须加REGEXP校验节点边界,例如CONCAT(', ', path, ', ') NOT REGEXP ', 42, '
查最短路径?别只靠 LIMIT 1,得用窗口函数排序
WITH RECURSIVE 本身不保证遍历顺序——它按执行批次展开,不是 BFS 或 DFS。想拿到从 A 到 B 的最短路径,不能简单 LIMIT 1,因为深度为 3 的路径可能比深度为 2 的先出来(取决于 JOIN 顺序和索引)。
正确做法是在最终 SELECT 中用 ROW_NUMBER() OVER (ORDER BY array_length(path)) 标序号,再过滤 rn = 1。如果还要支持权重(带 cost 的边),就得把 SUM(cost) 累积进 CTE,并按该字段排序。
- PostgreSQL 示例:结尾加
SELECT * FROM t WHERE node_id = 5 ORDER BY array_length(path) LIMIT 1 - 避免在递归 CTE 内部加
ORDER BY—— 大多数数据库会忽略,且影响计划稳定性 - SQLite 不支持窗口函数,得用两层嵌套:外层
SELECT ... FROM (WITH RECURSIVE ...) ORDER BY length(path) LIMIT 1
有向无环图(DAG)不等于安全,仍要防隐式循环
即使你确认图结构无环,SQL 执行时仍可能因数据变更或 JOIN 条件疏漏产生逻辑环。比如边表 edges 里漏加 WHERE from_node != to_node,或者递归中没排除自环边,node_id = 1 就会永远生成 1 → 1 → 1…。
另一个常见盲区是“起点不可达终点”,这时递归 CTE 返回空集,但程序可能没处理这个 case,直接取 result[0] 报 IndexError。调试时别只看执行计划,务必用小数据集手动验证路径是否存在。
- 上线前必测:插入一条自环边
INSERT INTO edges VALUES (1,1),看是否崩 - 生产环境建议加
depth上限,且该值要小于图直径的 2 倍(留容错空间) - 如果边带权重且需最短路,
WITH RECURSIVE效率远低于 Dijkstra 实现,超 100 节点就该切到应用层










