【SQLite 内核】查询计划器与统计:没有 ANALYZE 时的启发式与代价估计边界
内容提要
本文介绍SQLite查询计划器:通过EXPLAIN QUERY PLAN区分SEARCH/SCAN访问路径;ANALYZE写入sqlite_stat1统计信息,无统计时用默认猜测(表百万行、索引重复度10);连接顺序采用N3启发式算法(O(KN)),放弃穷举最优,但存在star-query贪心短板。文章强调SQLite计划器与PostgreSQL CBO复杂度目标不同,统计不自动更新,EQP格式仅供调试。
延伸解读
无统计时的默认猜测:确定性而非随机
文章明确指出,未运行 ANALYZE 时,SQLite 计划器并非随机决策,而是使用文档写明的默认猜测:表大小按 100 万行、索引重复度按 10 估算。这些数字直接决定优化路径的开关,例如 skip-scan 仅在重复度≥18 时才启用,而默认猜测 10 低于阈值,因此未分析时永远不会启用 skip-scan。这种保守且确定的设计,体现了嵌入式场景下对可预测性的追求。
N3 算法的代价与局限
SQLite 的 join 顺序选择采用 N3 启发式算法,以 O(KN) 的复杂度替代穷举搜索的 O(K!),在毫秒级完成规划。但官方文档承认,在 star-query 场景下,N3 可能因贪心策略而错过全局最优解,且修复仍在演进中。这提醒开发者,对于复杂分析型查询,SQLite 的计划质量可能不如 PostgreSQL 等 CBO 系统,需谨慎评估其适用性。
统计信息不会自动更新
ANALYZE 生成的统计信息是运行时刻的快照,不会随数据变化自动更新。数据分布发生显著变化后,若不重新执行 ANALYZE 或依赖 PRAGMA optimize,计划器可能基于过期统计做出次优决策。文章建议应用在关闭连接前调用 PRAGMA optimize,让 SQLite 自行判断是否需要重新分析,而非手工决定时机。
Q&A
SQLite 的 EXPLAIN QUERY PLAN 中 SEARCH 和 SCAN 有什么区别?
SEARCH 表示只访问表中一个子集,通常使用索引;SCAN 表示全表扫描,包括按索引顺序遍历全表的情况。区分标准是访问的是全表还是子集,而不是是否使用索引。
SQLite 没有运行 ANALYZE 时,查询计划器如何估计表的大小和索引重复度?
没有 ANALYZE 时,SQLite 使用文档中写明的默认猜测:表大小猜测为 100 万行,索引最左列平均重复 10 个值。这些默认值用于判断是否使用 skip-scan 或自动索引等优化。
SQLite 的 ANALYZE 命令写入哪些统计信息?sqlite_stat1 和 sqlite_stat4 分别存储什么?
ANALYZE 写入 sqlite_stat1 表,每行包含表名、索引名和统计信息,其中第一个数字是索引扫描的总行数估计,后续数字是索引最左列、最左两列等相同取值平均对应的行数。sqlite_stat4 在编译时启用 SQLITE_ENABLE_STAT4 时生成,存储直方图信息,用于范围查询的选择性估计。
SQLite 的 N3 启发式算法是什么?它和穷举搜索在复杂度上有何区别?
N3 是 SQLite 3.8.0 引入的 join 顺序选择算法,每一步保留 N 条候选路径,存储 O(N),计算 O(K*N),其中 K 是 join 的表数。穷举搜索的时间复杂度是 O(K!),超过 10 路 join 后耗时不可接受。N3 用有界的复杂度换取接近最优的计划,但可能错过全局最优。
SQLite 的查询计划器在 star-query 场景下有什么已知问题?
在 star-query(一个大事实表加多个小维度表)场景下,N3 算法可能因贪心策略而错过全局最优解,因为候选槽位可能被先扫描小维度表的局部最优路径占满。官方文档承认此问题,并提到 2024 或 2025 年后的版本实现了启发式方法来解决。
SQLite 的统计信息会自动更新吗?如何保持统计信息最新?
不会自动更新。ANALYZE 收集的统计信息是快照,不会随数据库内容变化而自动刷新。建议使用 PRAGMA optimize 在应用关闭连接前调用,让 SQLite 自行判断哪些表需要重新 ANALYZE。
EXPLAIN QUERY PLAN 的输出格式稳定吗?可以在应用代码中解析吗?
不稳定。官方文档明确说明 EXPLAIN QUERY PLAN 的输出仅供交互式调试使用,格式可能随版本变化。文档记录了 3.24.0 和 3.36.0 两次格式变化,因此不建议在应用代码中解析该输出。