土法炼钢兴趣小组的算法知识备份

【SQLite 内核】VDBE 字节码执行:prepare、step 与寄存器机

文章导航

分类入口
databasestorage
标签入口
#sqlite#vdbe#bytecode#prepare#sqlite3-step#explain#opcode#cursor

源码下载

本文相关源码已整理,共 1 个文件。

打开下载目录 →

目录

B-Tree 遍历与分裂 回答了「页里如何找 key」。应用侧真正调用的却是 sqlite3_prepare_v2sqlite3_step:前者把 SQL 编译成一段字节码程序,后者在虚拟机里这段程序,直到吐出一行、写完、或出错。官方 The SQLite Bytecode Engine 把这层叫 VDBE(Virtual DataBase Engine),并明确:prepared statement 大体上就是实现该 SQL 的 bytecode。

常见误区有两个极端:一是以为 SQLite「没有执行引擎、只是解析后直接摸 B-Tree」;二是以为 EXPLAIN 输出是稳定的公开 API,可以写进业务逻辑。本文只钉执行循环本身:

  1. prepare / step 与 bytecode 虚拟机的对应关系。
  2. 指令格式(opcode + P1–P5)、寄存器与 B-Tree 游标。
  3. 用本机 SQLite 3.53.2EXPLAIN 走通一次 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.cvdbe*.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 篇)。

常见误解

  1. 「每次 step 都重新解析 SQL。」
    解析与代码生成发生在 preparestep 只推进已生成的程序。反复执行同语句应复用 statement,或使用参数绑定。

  2. EXPLAIN 输出可以当稳定协议。」
    官方写明 opcode 细节随 release 变化;业务代码依赖 EXPLAIN 列含义会在升级时静默失败。

  3. 「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 打开游标并绑定根页;SeekRowidNextColumn 等在游标上移动或解出列。游标号通常出现在 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:虚拟机先跳到地址 7Transaction,再 Goto 回地址 1。因此有效顺序是:

  1. Transaction — 进入读事务(P5 等标志与语句 journal 相关;本例 usesStmtJournal=0)。
  2. OpenRead — 游标 0 打开表 t,根页 2(页 1 仍是 sqlite_schema,与第 2 篇一致)。
  3. Integer — 寄存器 r[1] = 1(绑定的 id)。
  4. SeekRowid — 在游标 0 上按整数 key 定位;找不到则跳 P2=6(Halt)。
  5. Column — 从游标解出列 1(name)到 r[2]
  6. ResultRow — 输出 r[2]step 返回 SQLITE_ROW
  7. 再次 step 时继续到 HaltSQLITE_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

常见误解(续)

  1. EXPLAINEXPLAIN QUERY PLAN 是一回事。」
    前者是 VDBE 指令列表;后者是高层访问路径(是否用索引等)。排计划问题优先后者(第 11 篇)。

  2. 「打开游标等于锁住整库。」
    锁状态机在第 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」。


六、小结

三句话小结

  1. sqlite3_prepare_v2 编译 SQL 为 bytecode,sqlite3_step 运行该程序;ResultRow 把一行寄存器内容交给列 API 并暂停虚拟机。
  2. 指令通过游标调用 B-Tree,不直接读文件;Pager 仍在游标之下(第 3–4 篇)。
  3. 本机 3.53.2 的点查 EXPLAIN 展示 Transaction → OpenRead(root=2) → SeekRowid → Column → ResultRow;opcode 细节非稳定 API,升级后必须重跑 EXPLAIN

参考资料

规范与官方文档(A 级)

  1. SQLite Documentation, The SQLite Bytecode Engine(sqlite.org/opcode.html)。
  2. SQLite Documentation, Why SQLite Uses Bytecode(sqlite.org)。
  3. SQLite C API:sqlite3_prepare_v2sqlite3_stepsqlite3_column_*

源码(A 级)

  1. vdbe.cvdbe*.c(opcode 实现与注释为权威来源)。

论文(A 级,讨论锚点)

  1. Gaffney, K. P., et al. SQLite: Past, Present, and Future. PVLDB 15(12), 2022. DOI: 10.14778/3554821.3554842(执行/分析瓶颈讨论框架;数字不冒充本站实测)。

实验

  1. 本机 SQLite 3.53.2:EXPLAIN SELECT name FROM t WHERE id=1;(输出见第三节;脚本 reproduce/02-header-explain.sh)。

站内

  1. B-Tree 遍历与分裂Pager 与 Page Cache单文件格式
  2. 本系列 indexPLAN.md

上一篇B-Tree 遍历与分裂
下一篇SQL 编译管线

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-07-18 · database / storage

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

拆开 sqlite3_prepare_v2 内部如何把 SQL 文本经词法、语法、名字解析、查询规划变成第 5 篇执行的 bytecode;用本机 3.53.2 的 EXPLAIN QUERY PLAN 钉住三种访问路径形态,并交代 schema cookie 触发 reprepare 的机制,规划算法细节留给第 11 篇。

2026-07-17 · database / storage

【SQLite 内核】单文件格式与页面头

拆解 SQLite 单文件格式:100 字节 database header 与 B-Tree 页面头的逐字段布局,用本机 3.53.2 CLI 实测 hexdump 核对 magic、page size、页类型标志,并钉住 file format 官方规范与 Bayer/McCreight 谱系。


By .