SQLite 递归 CTE 遍历层级:把访问路径带上,显式挡住循环
层级数据也可能混进一条回头边
分类菜单看起来是一棵树,但导入数据时可能把某个子分类重新指向祖先。递归查询若只不断连接下一层,就会在环里重复行。另一个容易误判的情况是共享分类:两个上级都引用同一节点,这并不等于循环。要保留所有展示路径,就必须区分“在当前路径见过”与“在其他路径见过”。
SQLite 的递归 CTE 由起点查询和递归查询组成。官方文档指出,UNION 按整行去重,UNION ALL 保留重复行。因此,当结果还携带深度或路径时,仅把后者改成前者不一定能防环:绕一圈之后路径已变长,整行仍然不同。下面直接把当前路径中的节点作为禁止再次进入的集合。
构造有共享节点和两类循环的样本
这段 SQL 可直接在 SQLite 中执行,不创建持久表。起点是一,两个分支都到达四,四又指回一;十一还有自环。最后将正常访问与被挡住的边一起输出。编号使用整数,路径用斜杠分隔;检测完整的“斜杠、编号、斜杠”,才能避免把十一误当作已经访问过的一。
AI生成概念示意图,非真实界面
WITH RECURSIVE
nodes(id, name) AS (
VALUES (1, 'root'), (2, 'guide'), (3, 'reference'),
(4, 'shared'), (11, 'archive')
),
edges(parent, child) AS (
VALUES (1, 2), (1, 3), (1, 11),
(2, 4), (3, 4), (4, 1), (11, 11)
),
walk(id, depth, path) AS (
SELECT id, 0, printf('/%d/', id)
FROM nodes WHERE id = 1
UNION ALL
SELECT e.child, w.depth + 1,
w.path || printf('%d/', e.child)
FROM walk AS w
JOIN edges AS e ON e.parent = w.id
JOIN nodes AS n ON n.id = e.child
WHERE instr(w.path, printf('/%d/', e.child)) = 0
AND w.depth < 8
),
report(status, id, depth, path) AS (
SELECT 'visit', id, depth, path FROM walk
UNION ALL
SELECT 'blocked-cycle', e.child, w.depth + 1,
w.path || printf('%d/', e.child)
FROM walk AS w
JOIN edges AS e ON e.parent = w.id
WHERE instr(w.path, printf('/%d/', e.child)) > 0
)
SELECT r.status, r.id, n.name, r.depth, r.path
FROM report AS r
JOIN nodes AS n ON n.id = r.id
ORDER BY r.status DESC, r.depth, r.path;正常访问应有六行:根、三个直接子节点,以及通过两个分支到达四的两行。被阻止的边应有三行,分别是十一回到自己,以及两条路径中的四回到一。人工验算时先画出这些边,再对照结果;仅看到查询很快结束,不能证明它既没有漏节点,也没有错误合并合法路径。
防环、去重与截断各自解决不同问题
路径列记录的是当前分支的历史,不是全局访问表。四出现两次是本例特意保留的信息;如果业务只要“所有可达分类”,可以在最终查询对编号去重,但这会丢掉具体入口。若要计算分类总数,必须先决定按路径算还是按唯一分类算,否则汇总结果可能把共享分类重复计入。
instr 在未找到子串时返回零,这正好作为进入下一节点的条件。此写法依赖整数编号不会包含分隔符;若换成任意文本标识,必须选择不会混淆的编码或结构化表示,不能把名称原样拼接。路径越长,字符串检测的工作越多;本例适合解释语义,不意味着大型关系网也应该直接照搬。
深度八是额外的保护线,根节点深度为零。它不是“八层以内就没有环”的证明,也不是完整性承诺。若真实数据到达边界,应继续检查该层是否还有未访问的子节点,并把结果标为可能截断。路径防环能避免沿同一分支无限回访,但共享分支很多时,可行路径数量仍可能迅速增加。
诊断行保留导致回访的完整路径,便于维护人员定位坏边,再决定该删除还是改挂到其他分类。
上线前保留可解释的检查样本
删掉两条坏边后,循环诊断应清空,而六条合法访问路径应保留;这能检查防环条件有没有误伤正常关系。
把起点改成不存在的编号,应返回空集。另行检查指向不存在节点的边,因为这里的连接会把它们排除。
最终排序只约定展示顺序,不要据此推断数据库内部的遍历顺序。扩展为实体表后,应检查父节点连接列的索引与实际查询规模。


