SQLite 能否在一个递归 CTE 中遍历父子链表并计算层级?
数据小许
结论
如果 idx 需要根据现有的 prev/next 链表动态计算,通常不适合直接把两段递归合并成一个简单的递归 CTE:一段沿兄弟链计算位置,另一段沿父子关系遍历树。也可以用携带遍历栈的状态机尝试一次遍历,但 SQL 会复杂得多。
如果能在每条父子关系上保存子节点的整数位置,例如 position,一个递归 CTE 就可以同时计算层级和位置,并按树的先序顺序输出。
推荐做法:保存子节点位置
表结构可以改为:
CREATE TABLE map (
parent,
child,
position
);
每个父节点的子节点按 position 从小到大排列。递归时,level 由父节点层级加一得到,idx 则直接取当前边上的 position:
WITH RECURSIVE tree(child, level, idx, path) AS (
VALUES (0, 0, 0, '')
UNION ALL
SELECT
m.child,
t.level + 1,
m.position,
t.path || printf('%010d.', m.position)
FROM map AS m
JOIN tree AS t ON m.parent = t.child
)
SELECT
substr('..........', 1, level * 3) || child AS node,
level,
idx
FROM tree
ORDER BY path;
path 会累积每个节点祖先路径上的位置,用来保证先序输出:先输出一个节点,再输出它的子树,最后输出同层的下一个节点。printf('%010d.', ...) 会把位置补齐到固定宽度,方便按文本排序。如果位置可能超过 10 位,需要相应增加宽度。示例以根节点 0 作为递归起点,因此不需要把它放进 map 表。
保留 prev/next 时
在现有结构中,位置不是节点的直接属性。需要从 prev is null 的首节点出发,沿 next 链逐个计数,再按 parent 使用这些位置遍历树。因此,把原查询拆成两个递归步骤会更清晰。如果运行时必须只执行一次递归,可以设计一个显式的深度优先遍历状态,把当前节点和回溯所需的信息放进递归状态。这种写法要处理进入子树、访问兄弟节点和回溯等分支,维护起来比存储 position 更复杂。
使用前检查
确认每个父节点的子节点链表只有一个 prev IS NULL 的首节点,链表没有断链或环,并且每个子节点只属于一个父节点。否则,链表遍历可能漏行或重复,层级结果也可能不符合预期。
备注:内容仅供参考。
