【SQLite 内核】SQL 编译管线:从文本到 VDBE 程序

💡 原文中文,约11100字,阅读约需27分钟。
📝

内容提要

SQLite的SQL编译管线分为Tokenizer、Parser、名字解析和代码生成四段,其中查询规划与代码生成无独立边界。schema cookie变化时,sqlite3_step会根据语句是否由prepare_v2创建自动重编译或返回SQLITE_SCHEMA。实测显示INT与INTEGER PRIMARY KEY对同一查询产生不同访问路径,规划算法采用NN/N3启发式,不保证全局最优。

🔎

延伸解读

INT 与 INTEGER 的陷阱

实测显示,INT PRIMARY KEY 与 INTEGER PRIMARY KEY 在 SQLite 中语义不同:只有精确写作 INTEGER PRIMARY KEY 的列才被当作 rowid 别名,而 INT PRIMARY KEY 会被视为普通 UNIQUE 约束,自动创建索引。这导致同一查询产生不同的访问路径和字节码,例如点查时前者需回表,后者直接定位。开发者应留意这一细微差别,避免因类型写法不同而影响查询性能。

schema cookie 与语句过期

SQLite 通过 schema cookie(文件头偏移 40 处的整数)跟踪 schema 变化。每次 step 时,若 cookie 不匹配,prepare_v2/v3 创建的语句会自动用保存的 SQL 文本重新编译,而旧版 prepare 则返回 SQLITE_SCHEMA 错误。这意味着长连接中频繁的 ALTER TABLE 或索引创建可能导致 prepared statement 悄悄重编译,引起查询延迟抖动,应用层需注意这一隐性开销。

查询规划器的启发式局限

SQLite 的查询规划器采用 NN/N3 启发式算法,而非穷举或动态规划,以换取在 64 路 join 等复杂场景下的快速规划。代价是不保证全局最优,尤其在 star-schema 查询中可能因候选集限制而选错 join 顺序。官方建议通过 PRAGMA optimize 补充统计信息来缓解,而非手工改写查询,这体现了嵌入式数据库在性能与最优性之间的权衡。

Q&A

SQLite 的 SQL 编译管线分为哪几个阶段?

SQLite 的 SQL 编译管线分为四个阶段:Tokenizer(词法分析)、Parser(语法分析)、名字解析(resolve.c)以及查询规划与代码生成(where*.c/select.c 等)。其中,Tokenizer 将 SQL 文本切分为 token 流,Parser 将 token 组装成语法树,名字解析将标识符绑定到具体的表和列,查询规划与代码生成则负责选择访问路径并生成 VDBE 字节码。

SQLite 的查询规划器是独立模块吗?

不是。SQLite 的查询规划器没有独立的模块或产物,它内嵌在代码生成器中,与代码生成共享同一批文件(如 where*.c 和 select.c),并在同一次遍历中完成。规划器的输出是 WhereInfo/WhereLoop 结构,随后立即被翻译成 VDBE 指令。

SQLite 的 schema cookie 是什么?它如何影响 prepared statement?

schema cookie 是数据库文件头部偏移 40 处的 4 字节大端整数,每次 schema 变化(如 CREATE、DROP、ALTER TABLE)都会递增。当 sqlite3_step 执行时,会检查当前 schema cookie 是否与编译时一致。如果不一致,对于使用 sqlite3_prepare_v2/v3 创建的语句,会自动使用保存的 SQL 文本重新 prepare 并重跑;对于旧版 sqlite3_prepare 创建的语句,则返回 SQLITE_SCHEMA 错误。

INT PRIMARY KEY 和 INTEGER PRIMARY KEY 在 SQLite 中有什么区别?

在 SQLite 中,只有精确写作 INTEGER PRIMARY KEY 的列才会被当作 rowid 的别名,而 INT PRIMARY KEY 会被当作普通的 UNIQUE 约束,并自动创建一个索引。因此,对于同一查询,使用 INT PRIMARY KEY 时,EXPLAIN QUERY PLAN 会显示使用自动索引(如 sqlite_autoindex_t_1),而使用 INTEGER PRIMARY KEY 时,会直接使用 rowid 进行查找,访问路径不同。

SQLite 的查询规划算法是什么?它保证全局最优吗?

SQLite 的查询规划算法在 3.8.0 之前使用 Nearest Neighbor(NN)启发式,之后改用 N Nearest Neighbors(N3)启发式。这些算法都是贪心或近似搜索,不保证全局最优,但能在 join 表数很多时快速生成计划。SQLite 选择这种设计是为了在嵌入式场景下获得更快的编译速度,而不是追求最优计划。

为什么 ALTER TABLE RENAME COLUMN 之后,旧的 prepared statement 不能直接复用?

因为名字解析在 prepare 阶段就将列名绑定为内部的游标号和列号,执行期不再使用列名字符串。当列名改变时,schema cookie 会变化,导致 prepared statement 过期,需要重新编译。对于使用 prepare_v2/v3 创建的语句,会自动重新 prepare;对于旧版 prepare,则会返回 SQLITE_SCHEMA 错误。

EXPLAIN QUERY PLAN 和 EXPLAIN 有什么区别?

EXPLAIN QUERY PLAN 提供高层的访问路径描述,如 SEARCH、SCAN、USE TEMP B-TREE,用于判断是否使用了正确的索引;而 EXPLAIN 输出逐条 VDBE opcode,用于分析执行细节。两者都不是稳定协议,输出可能随版本变化。

🏷️

标签

➡️

继续阅读