本节摘要:VDBE 是 SQLite 独有的执行层:寄存器式虚拟机,指令集约 180 个操作码。本节先讲清寄存器与游标两个核心概念,再逐行分析一条点查的完整字节码,最后对照 MySQL 执行器与 PostgreSQL 火山模型,说明"先编译成指令序列"这条路线的得与失。
VDBE 的世界只有两样活动的东西。寄存器是临时值的容器:参数、列值、比较结果、聚合中间量都住在里面,编号从 1 开始。**游标(cursor)**是指向某个 B-Tree 的可移动指针:每打开一张表或一个索引参与查询,编译器就分配一个游标号,SeekRowid、Next、Prev 这些指令都在推动游标移动。
一条指令的格式是五个字段:addr opcode p1 p2 p3 p4 comment——地址、操作码、最多三个整数操作数、一个附加参数(通常是字符串)、人读的注释。地址从 0 编号,跳转指令直接写目标地址,所以字节码本质上是一段可以反向跳转的直线程序。
准备一张最简表,然后让虚拟机自己说话:
CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT); EXPLAIN SELECT name FROM users WHERE id = 42;
输出(版本不同行号与细节略有出入,结构一致):
addr opcode p1 p2 p3 p4 p5 comment ---- ---------- --- --- --- -------- -- -------------------- 0 Init 0 1 0 00 Start at 1 1 OpenRead 0 2 0 2 00 root=2 iDb=0; users 2 Integer 42 1 0 00 r[1]=42 3 SeekRowid 0 7 1 00 intkey=r[1] 4 Column 0 1 2 00 r[2]=users.name 5 ResultRow 2 1 0 00 output=r[2] 6 Goto 0 7 0 00 7 Halt 0 0 0 00
逐行拆解这条指令流:
root=2 正是第 2 章 sqlite_schema 里那张表的根页,章节之间的知识在这里咬合。p4 的 2 是列数提示。WHERE id = ?,这一步会被 Variable 指令替代,从绑定槽取值。r[1] 的 rowid 定位;找不到就跳到 7 号 Halt 结束。树查找在指令内部完成,第 2 章算的"两三次页访问"就是它。
MySQL 的执行器与 PostgreSQL 的执行器都不生成指令序列。PostgreSQL 采用火山模型:计划树每个节点实现一个 next 接口,顶层节点不断向上层"要下一行",行像岩浆一样从叶子节点逐行喷到顶。MySQL 8.0 引入了 Volcano 迭代器模型的现代化版本,同时也为复杂查询准备了批量物化的补充路径。三种模型的差异在四件事上:
| 关注点 | SQLite VDBE | MySQL 执行器 | PostgreSQL 火山模型 |
|---|---|---|---|
| 编译产物 | 指令序列,可序列化可缓存 | 计划对象,会话内复用 | 计划树,可通用可定制 |
| 每行成本 | 指令循环切换,极轻 | 迭代器虚调用 | 逐节点函数调用链 |
| 调试工具 | EXPLAIN 显示全部指令 | EXPLAIN 显示计划表 | EXPLAIN 显示计划树 |
| 极致优化的方式 | 手写专用操作码 | 调度器特化 | JIT 编译为机器码 |
火山模型逐行调用链在解释执行下有可观开销,PostgreSQL 的对策是对重查询上 JIT(把表达式编译成机器码);SQLite 的对策是更朴素的——指令循环本身就轻,虚拟机调度成本被压到纳秒级。两条路线再次印证全册主线:嵌入式选择把复杂度前移到编译期,服务端选择在运行期动态补足。
再看两个常用片段,建立"读码直觉"。范围扫描会有循环:
4 SeekGE 1 9 1 -- 索引游标定位到首个命中键 5 IdxGT 1 9 1 -- 越界即跳 Halt,循环出口 6 Column 1 0 2 -- 从索引取列 7 ResultRow 2 1 0 8 Next 1 4 0 -- 移到下一索引项,跳回 4
聚合查询会看到 AggStep 与 AggFinal 这对指令——分步累加与终值计算分离,配合排序器指令 SorterOpen、SorterSort、SorterNext,排序聚合类查询的执行轮廓在字节码里一览无余。练习建议:拿你自己业务里最慢的一条查询跑 EXPLAIN,数一数触碰游标的指令条数,再对照 EXPLAIN QUERY PLAN 看访问路径是否合理——这套动作在第 5 章会升级成完整的调优流程。
**指令清单里的 p4 列是什么?**p4 是第四操作数,多数指令用不到,用到时通常携带字符串或结构指针:OpenRead 的 p4 是列数提示,字符串比较指令的 p4 是排序规则(BINARY 或 NOCASE),聚合指令的 p4 是函数名。读不认识的指令先看 p4——它往往直接说明这条指令在跟哪种资源打交道。
**字节码有没有"调试器"?**EXPLAIN 就是反汇编视图,此外 sqlite3 命令行的 .eqp full 模式会在每条语句后同时打印计划与完整字节码。虚拟机没有断点单步能力——它是嵌入式引擎,调试预期是"读懂清单"而不是"逐步跟踪"。真要看运行时行为,给应用包一层语句 trace 回调,把每条执行的 SQL 与耗时记下来,配合字节码清单还原现场。
下一节把字节码视角接回工程实践:三个引擎的 EXPLAIN 长什么样,预编译语句怎么管。