PAYATHON 2026

SQLite 递归 CTE 的初始查询为何显示 SCAN?

数据阿明

结论

SETUP 阶段出现 SCAN node,是因为递归 CTE 的锚点查询读取了 node 表中的所有行,没有按主键筛选。若要从节点 1 开始遍历,应在锚点查询中添加 WHERE id = 1。这样 SQLite 才能将初始查询规划为主键查找。

原查询的锚点部分是:

SELECT id AS id_first, id, content
FROM node

WHERE id_first = 1 写在递归 CTE 外层,只会过滤 CTE 生成的结果,不会限制锚点查询从 node.id = 1 开始读取。递归 CTE 的锚点负责生成起始行,递归部分再从这些行继续遍历。

修改查询

将起点条件放到锚点查询中:

EXPLAIN QUERY PLAN
WITH RECURSIVE
chain AS
(
    SELECT id AS id_first, id, content
    FROM node
    WHERE id = 1

    UNION ALL

    SELECT chain.id_first, node.id, node.content
    FROM chain
    INNER JOIN link ON link.id_src = chain.id
    INNER JOIN node ON node.id = link.id_dst
)
SELECT * FROM chain;

如果起点由运行时传入,可以使用参数,例如 WHERE id = ?,并在执行查询时绑定起点 ID。

如何理解这份查询计划

原计划中的 SCAN node 表示锚点查询没有查找条件来定位特定主键,因此需要遍历 node 的行以生成初始结果。对于这里的 node 表,这可能意味着扫描整张表。它不代表递归部分在查找下一节点时也没有使用索引或主键。

计划中的另外两项说明了递归步骤如何查找数据:

  • SEARCH link USING PRIMARY KEY (id_src=?):根据当前节点,按 link 的复合主键查找相连的链接。
  • SEARCH node USING INTEGER PRIMARY KEY (rowid=?):通过 node.id 的整数主键查找目标节点。在这种表定义下,SQLite 的整数主键对应 rowid。

加入锚点条件后,node.id 的值已知,SQLite 通常可以在锚点查询中使用整数主键查找。EXPLAIN QUERY PLAN 的具体文本可能因 SQLite 版本和查询细节而异,应以实际运行版本输出的计划为准。

注意事项

如果查询需要从表中的每个节点开始生成链,锚点不加条件并扫描 node 符合查询语义。如果只需从某个节点开始,应在锚点中筛选起点。仅在 CTE 外层过滤结果,无法表达相同的起点限制。

备注:内容仅供参考。