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 外层过滤结果,无法表达相同的起点限制。
备注:内容仅供参考。
