B-Tree
遍历与分裂 回答了「页里如何找
key」。应用侧真正调用的却是 sqlite3_prepare_v2
与 sqlite3_step:前者把 SQL
编译成一段字节码程序,后者在虚拟机里跑这段程序,直到吐出一行、写完、或出错。官方
The SQLite Bytecode Engine 把这层叫
VDBE(Virtual DataBase
Engine),并明确:prepared statement
大体上就是实现该 SQL 的 bytecode。
常见误区有两个极端:一是以为
SQLite「没有执行引擎、只是解析后直接摸 B-Tree」;二是以为
EXPLAIN 输出是稳定的公开
API,可以写进业务逻辑。本文只钉执行循环本身:
prepare/step与 bytecode 虚拟机的对应关系。- 指令格式(opcode + P1–P5)、寄存器与 B-Tree 游标。
- 用本机 SQLite 3.53.2 的
EXPLAIN走通一次SELECT name FROM t WHERE id=1,把路径接到第 3–4 篇的 Pager / B-Tree。
SQL 如何被解析、优化、生成这些 opcode,留给 第 6 篇。
本文是「SQLite 内核」系列第 5 篇(共 17 篇)。→ 系列目录
篇目 核心内容 第 4 篇 · B-Tree 遍历与分裂 table/index b-tree、分裂平衡 第 5 篇 · VDBE 字节码执行 prepare / step、寄存器、EXPLAIN 点查 第 6 篇 · SQL 编译管线 词法语法 → 代码生成
版本锚定:官方 The SQLite Bytecode Engine(sqlite.org/opcode.html);Why SQLite Uses Bytecode。源码主文件
vdbe.c及vdbe*.c辅助文件。Opcode 名称与语义随版本变化——研究EXPLAIN时必须对照同版本文档或源码注释;本文实测与解释锚定本机 SQLite 3.53.2。官方明确:bytecode 引擎不是稳定 API,应用不得依赖细节。
一、两条 API:编译与执行
| API | 角色 | 官方表述 |
|---|---|---|
sqlite3_prepare_v2 |
编译器 | 把 SQL 译成 bytecode |
sqlite3_step |
虚拟机 | 运行 prepared statement 内的 bytecode |
sqlite3_finalize |
回收 | 释放程序与游标 |
执行从指令地址 0 开始,直到
Halt、程序计数器越界、或错误。出错时未决事务回滚、已分配内存与打开的游标被清理(Bytecode
Engine §2.2)。
flowchart LR
sql["SQL text"] --> prep["sqlite3_prepare_v2"]
prep --> prog["Bytecode program<br/>prepared statement"]
prog --> step["sqlite3_step loop"]
step -->|"ResultRow"| row["SQLITE_ROW<br/>column APIs read registers"]
step -->|"Halt OK"| done["SQLITE_DONE"]
step -->|"error"| err["Error + rollback"]
row --> step
1.1 为什么是 bytecode,而不是立刻执行树
官方 Why SQLite Uses Bytecode
给出工程动机:便于可移植、便于 EXPLAIN
观测、便于在一条语句内表达分支与子程序。对本系列读者更直接的后果是——热路径上的「执行」与「页面
I/O」被切开:VDBE 发游标操作,Pager/B-Tree
兑现页访问(第 3–4 篇),journal/WAL 兑现原子提交(第 7–8
篇)。
常见误解
「每次
step都重新解析 SQL。」
解析与代码生成发生在prepare;step只推进已生成的程序。反复执行同语句应复用 statement,或使用参数绑定。「
EXPLAIN输出可以当稳定协议。」
官方写明 opcode 细节随 release 变化;业务代码依赖EXPLAIN列含义会在升级时静默失败。「VDBE 寄存器就是 CPU 寄存器。」
这里是虚拟机寄存器文件:固定数量的槽,每个槽可持 NULL、整数、浮点、文本、BLOB 等(Bytecode Engine §2.3)。
二、指令格式、寄存器与游标
2.1 五操作数指令
每条指令含一个 opcode 与操作数 P1–P5:
| 操作数 | 类型 | 常见用途 |
|---|---|---|
| P1 | 32-bit int | 游标号、或寄存器号 |
| P2 | 32-bit int | 跳转目标、或寄存器号 |
| P3 | 32-bit int | 另一寄存器 / 辅助参数 |
| P4 | 多态 | 字符串、collation、函数指针、大整数等 |
| P5 | 16-bit flags | 细微行为开关(如 NULL 比较语义) |
并非每条指令用满五个操作数。
2.2 寄存器与 ResultRow
查询在发出 ResultRow
之前,把当前行的列值装进一组寄存器;sqlite3_column_*
从这些寄存器取值。ResultRow 使当前
sqlite3_step 返回
SQLITE_ROW,并暂停虚拟机;下一次
step 从下一条指令继续(Bytecode Engine
§2.2)。
2.3 B-Tree 游标
对表/索引的扫描与定位通过 cursor
完成:OpenRead / OpenWrite
打开游标并绑定根页;SeekRowid、Next、Column
等在游标上移动或解出列。游标号通常出现在 P1。这把第 4 篇的
B-Tree 查找接到字节码层——VDBE 不自己做页二分,它调用 B-Tree
API。
flowchart TB
subgraph vdbe ["VDBE"]
op["Opcodes"]
reg["Register file"]
cur["Cursor table"]
end
subgraph storage ["Storage path"]
bt["B-Tree"]
pg["Pager / cache"]
file["Database file"]
end
op --> reg
op --> cur
cur --> bt
bt --> pg
pg --> file
三、实测:一次点查的 EXPLAIN
环境:本机 sqlite3
3.53.2;库由 reproduce/02-header-explain.sh
创建(PRAGMA page_size=4096;表
t(id INTEGER PRIMARY KEY, name TEXT);一行
id=1)。以下输出经本机执行,注释列以 CLI
为准。
sqlite3 sk-header.db "EXPLAIN SELECT name FROM t WHERE id=1;"addr opcode p1 p2 p3 p4 p5 comment
---- ------------- ---- ---- ---- ------------- -- -------------
0 Init 0 7 0 0 Start at 7
1 OpenRead 0 2 0 2 0 root=2 iDb=0; t
2 Integer 1 1 0 0 r[1]=1
3 SeekRowid 0 6 1 0 intkey=r[1]
4 Column 0 1 2 0 r[2]= cursor 0 column 1
5 ResultRow 2 1 0 0 output=r[2]
6 Halt 0 0 0 0
7 Transaction 0 0 1 0 1 usesStmtJournal=0
8 Goto 0 1 0 0
3.1 按控制流读,而不是按地址顺序
Init 的 P2=7:虚拟机先跳到地址
7 做 Transaction,再 Goto
回地址 1。因此有效顺序是:
- Transaction — 进入读事务(P5
等标志与语句 journal 相关;本例
usesStmtJournal=0)。 - OpenRead — 游标 0 打开表
t,根页 2(页 1 仍是sqlite_schema,与第 2 篇一致)。 - Integer — 寄存器
r[1] = 1(绑定的 id)。 - SeekRowid — 在游标 0 上按整数 key
定位;找不到则跳 P2=6(
Halt)。 - Column — 从游标解出列
1(
name)到r[2]。 - ResultRow — 输出
r[2],step返回SQLITE_ROW。 - 再次
step时继续到 Halt →SQLITE_DONE。
sequenceDiagram
participant App
participant Step as sqlite3_step
participant V as VDBE
participant BT as B-Tree
App->>Step: first step
Step->>V: run from Init
V->>V: Transaction then OpenRead
V->>BT: SeekRowid key=1
BT-->>V: positioned
V->>V: Column into r2
V-->>Step: ResultRow
Step-->>App: SQLITE_ROW
App->>Step: next step
Step->>V: resume after ResultRow
V-->>Step: Halt
Step-->>App: SQLITE_DONE
3.2 这条路径证明了什么
| 观察 | 机制含义 |
|---|---|
| 根页 = 2 | 用户表不在页 1;与 file format / 第 2 篇一致 |
SeekRowid |
主键点查走 table b-tree 整数 key,不是全表扫 |
Column + ResultRow |
行值经寄存器交给 C API,而不是「SQL 字符串结果」 |
无 Rewrite / 优化器 opcode |
计划已在 prepare 时折叠进这段程序;第 6、11 篇再拆生成侧 |
复现脚本与库生成步骤见 reproduce/02-header-explain.sh。换
SQLite 小版本后,opcode 名或地址布局可能变——以你本机
EXPLAIN 为准核对,不要死背本页地址表。
四、与相邻层的边界
| 层 | 本篇 | 其他篇 |
|---|---|---|
| SQL → bytecode | 只承认 prepare 是编译入口 |
第 6 篇管线 |
| 游标 → 页 | OpenRead / SeekRowid /
Column |
第 3–4 篇 Pager/B-Tree |
| 提交原子性 | Transaction 出现即可 |
第 7–8、10 篇 |
| 计划好坏 | 本例计划「看起来合理」 | 第 11–12 篇用 EXPLAIN QUERY PLAN |
常见误解(续)
「
EXPLAIN与EXPLAIN QUERY PLAN是一回事。」
前者是 VDBE 指令列表;后者是高层访问路径(是否用索引等)。排计划问题优先后者(第 11 篇)。「打开游标等于锁住整库。」
锁状态机在第 9 篇;读事务与写锁升级是另一条线。本篇的OpenRead只建立游标与根页绑定。
五、学术谱系、工程间隙与开放问题
谱系:把声明式查询编译成可移植指令流,是数据库系统的经典分工(System R 以降的「编译 / 执行」分离)。SQLite 的选择是进程内寄存器机 + 显式 B-Tree 游标 opcode,而不是把执行完全推到服务器进程的火山迭代器树。Gaffney et al.(PVLDB 2022)在讨论分析负载时,执行引擎与算子实现仍是瓶颈讨论的一部分——本篇不复述其实验数字,只标明:字节码层是后续向量化 / 代码生成争论的底座,而不是终点。
工程间隙:文档中的 opcode 表由
vdbe.c 注释生成,与具体 release
绑定。第三方工具若缓存「某版本 EXPLAIN
模板」跨版本复用,会与真实程序漂移。生产排障应:固定 SQLite
版本 → 对本机 EXPLAIN → 再对照同版本 opcode
文档。
开放问题:是否应在嵌入式场景引入更激进的执行策略(JIT、向量化批处理)——PVLDB 2022 与 DuckDB 等从不同方向探索;SQLite 主线仍以可移植 bytecode 为主。本系列第 17 篇选型时再对照,此处不预判「主线必须 JIT」。
六、小结
三句话小结
sqlite3_prepare_v2编译 SQL 为 bytecode,sqlite3_step运行该程序;ResultRow把一行寄存器内容交给列 API 并暂停虚拟机。- 指令通过游标调用 B-Tree,不直接读文件;Pager 仍在游标之下(第 3–4 篇)。
- 本机 3.53.2 的点查
EXPLAIN展示Transaction → OpenRead(root=2) → SeekRowid → Column → ResultRow;opcode 细节非稳定 API,升级后必须重跑EXPLAIN。
参考资料
规范与官方文档(A 级)
- SQLite Documentation, The SQLite Bytecode Engine(sqlite.org/opcode.html)。
- SQLite Documentation, Why SQLite Uses Bytecode(sqlite.org)。
- SQLite C
API:
sqlite3_prepare_v2、sqlite3_step、sqlite3_column_*。
源码(A 级)
vdbe.c及vdbe*.c(opcode 实现与注释为权威来源)。
论文(A 级,讨论锚点)
- Gaffney, K. P., et al. SQLite: Past, Present, and Future. PVLDB 15(12), 2022. DOI: 10.14778/3554821.3554842(执行/分析瓶颈讨论框架;数字不冒充本站实测)。
实验
- 本机 SQLite
3.53.2:
EXPLAIN SELECT name FROM t WHERE id=1;(输出见第三节;脚本reproduce/02-header-explain.sh)。
站内
上一篇:B-Tree
遍历与分裂
下一篇:SQL
编译管线
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【SQLite 内核】嵌入式行存全景:单文件、单写者、零 IPC
定位单文件嵌入式行存在服务器行存与 LSM 嵌入 KV 之间的生态位;钉住 SQLite 架构约束、站内分工与 17 篇阅读路线,并以 Bayer/McCreight、官方 file format、PVLDB 2022 为学术锚点。
【SQLite 内核】SQL 编译管线:从文本到 VDBE 程序
拆开 sqlite3_prepare_v2 内部如何把 SQL 文本经词法、语法、名字解析、查询规划变成第 5 篇执行的 bytecode;用本机 3.53.2 的 EXPLAIN QUERY PLAN 钉住三种访问路径形态,并交代 schema cookie 触发 reprepare 的机制,规划算法细节留给第 11 篇。
【SQLite 内核】单文件 · Pager · B-Tree · VDBE · WAL · 锁
补齐嵌入式行存内核层:从单文件格式、Pager/B-Tree、VDBE 到 Rollback Journal/WAL、锁状态机与计划器,并以 PG/InnoDB、DuckDB、RocksDB 对照收束;承接 sqlite-billion-rows 性能叙事。
【SQLite 内核】单文件格式与页面头
拆解 SQLite 单文件格式:100 字节 database header 与 B-Tree 页面头的逐字段布局,用本机 3.53.2 CLI 实测 hexdump 核对 magic、page size、页类型标志,并钉住 file format 官方规范与 Bayer/McCreight 谱系。