3.2 VDBE 虚拟机与字节码


3.2 VDBE 虚拟机:字节码指令流分析

本节摘要: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

逐行拆解这条指令流:

  • 0 Init:所有程序从 0 开始,Init 把指令指针跳到 p2(这里是 1),并完成事务环境的初始化检查。
  • 1 OpenRead:分配 0 号游标,指向根页号为 2 的 B-Tree——root=2 正是第 2 章 sqlite_schema 里那张表的根页,章节之间的知识在这里咬合。p4 的 2 是列数提示。
  • 2 Integer:把字面量 42 放进 1 号寄存器——这就是绑定参数的落点:若原语句写的是 WHERE id = ?,这一步会被 Variable 指令替代,从绑定槽取值。
  • 3 SeekRowid:本查询的心脏。在 0 号游标的表树上按 r[1] 的 rowid 定位;找不到就跳到 7 号 Halt 结束。树查找在指令内部完成,第 2 章算的"两三次页访问"就是它。
  • 4 Column:从游标 0 当前行的 1 号列(name,0 号列是 id)取值放进 2 号寄存器。
  • 5 ResultRow:宣布"一行结果就绪",虚拟机暂停(SQLITE_ROW),等应用取走寄存器 2 起的一列值,再从下一条继续。
  • 6 Goto:无条件跳到 7 收尾——单行点查没有循环,这条只是形式上的闭合。
  • 7 Halt:正常终止。

图:VDBE 指令流与数据通路

图:VDBE 指令流与数据通路

对照:没有虚拟机的两个执行器

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 与耗时记下来,配合字节码清单还原现场。

本节要点回顾

  • VDBE 是寄存器式虚拟机:寄存器传值、游标指树、约 180 个操作码组成直线程序。
  • 读字节码的口诀:先找触碰游标的指令(SeekRowid、SeekGE、Next、Column),它们才是有 IO 成本的地方。
  • 编译与执行分离使指令序列可缓存可复用,这是预编译语句的底层依据。
  • MySQL 与 PostgreSQL 解释执行计划,PostgreSQL 用 JIT 补解释开销;SQLite 用轻量指令循环把调度成本压到纳秒级。

下一节把字节码视角接回工程实践:三个引擎的 EXPLAIN 长什么样,预编译语句怎么管。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U