6.1 源码阅读:SQLite 架构概述
SQLite 是一个无服务器、零配置、事务性的 SQL 数据库引擎。整个数据库就是一个文件,整个引擎就是一个 C 库——没有独立的服务进程,没有网络协议,链接进你的程序就能用。大约 15 万行 C 代码,却被部署在几乎所有手机、浏览器和操作系统中,是世界上部署量最大的数据库引擎。
本文从顶向下拆解 SQLite 的架构。读完后你会理解:一条 SQL 语句从进入 API 到读写磁盘,经过了哪些层,以及每一层用了哪些关键的 C 数据结构和技术。
源码:https://sqlite.org/src,公有领域(Public Domain)。本文基于 SQLite 3.46 版本的合并源码(amalgamation)。
1. 为什么读 SQLite
SQLite 是学习「C 语言工程实践」的顶级教材:
- 单文件编译:官方提供合并源码
sqlite3.c,一个文件包含全部实现,便于阅读和搜索。 - 零依赖:除标准 C 库和操作系统接口外,不依赖任何第三方库。
- 高密度的 C 技巧:查找表优化、字节码虚拟机、B-tree 存储引擎、跨平台 VFS 抽象——每一层都是真实世界 C 编程的范例。
- 极高的测试覆盖率:100% 分支覆盖,测试代码量是实现代码的数百倍。
2. 架构总览
SQLite 的分层架构
SQLite 的实现从上到下分为七层。最上层是面向用户的 C API,最下层是操作系统的文件 I/O。一条 SQL 语句自顶向下流过每一层,最终变成对磁盘文件的读写操作:
| 层 | 职责 | 核心源文件 |
|---|---|---|
| 接口层 (Interface) | 公开 API,管理数据库连接 | main.c |
| 词法分析 (Tokenizer) | 将 SQL 文本切分为 token | tokenize.c |
| 语法分析 (Parser) | 将 token 流解析为语法树 | parse.y (经 Lemon 生成 parse.c) |
| 代码生成 (Code Generator) | 将语法树翻译为字节码 | build.c, select.c, where.c, expr.c |
| 虚拟机 (VDBE) | 执行字节码指令 | vdbe.c, vdbeaux.c |
| B-tree | 管理数据页的树形索引 | btree.c |
| Pager | 页缓存、事务、崩溃恢复 | pager.c, wal.c |
| OS 接口 (VFS) | 抽象文件和锁操作 | os_unix.c, os_win32.c |
2.1 接口层
接口层是用户唯一直接接触的部分。SQLite 的全部公开 API 都以 sqlite3_ 为前缀,定义在 sqlite3.h 中,实现在 main.c 中。
最核心的四个函数构成了一条 SQL 语句的完整生命周期:
int sqlite3_open(const char *filename, sqlite3 **ppDb);
int sqlite3_prepare_v2(sqlite3 *db, const char *zSql, int nByte,
sqlite3_stmt **ppStmt, const char **pzTail);
int sqlite3_step(sqlite3_stmt *pStmt);
int sqlite3_finalize(sqlite3_stmt *pStmt);2
3
4
5
sqlite3_open 打开(或创建)一个数据库文件,返回一个 sqlite3 * 连接句柄。内部实现 openDatabase 展示了典型的 C 语言资源初始化模式:
static int openDatabase(
const char *zFilename,
sqlite3 **ppDb,
unsigned int flags,
const char *zVfs
){
sqlite3 *db;
int rc;
*ppDb = 0;
rc = sqlite3_initialize();
if( rc ) return rc;
/* 分配并零初始化 sqlite3 结构体 */
db = sqlite3MallocZero( sizeof(sqlite3) );
if( db==0 ) goto opendb_out;
/* ... 设置互斥锁、编码、默认页大小 ... */
/* ... 打开 B-tree 存储、加载 schema ... */
*ppDb = db;
return rc;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
注意 goto opendb_out 的用法——这是 C 语言中处理多步初始化失败时的标准做法:所有清理逻辑集中在函数末尾的 opendb_out 标签处,避免在每个错误点重复释放代码。
2.2 词法分析器
词法分析器在 tokenize.c 中实现,核心是一个函数 sqlite3GetToken,负责从 SQL 字符串中识别出下一个 token 并返回其长度和类型。
SQLite 的词法分析器没有使用 lex/flex 等工具生成,而是手写的。它用字符分类查找表 aiClass[] 将 ASCII 字符映射到 30 种分类(character class),然后对分类做 switch 分发:
#define CC_SPACE 7 /* 空白字符 */
#define CC_MINUS 11 /* '-',可能是减号或 SQL 注释 */
#define CC_SLASH 16 /* '/',可能是除号或 C 风格注释 */
#define CC_LT 12 /* '<',可能是 < 或 <= 或 <> */
#define CC_DIGIT 3 /* 数字 */
#define CC_KYWD0 1 /* 关键字首字母 */
/* ... 共 30 种分类 ... */2
3
4
5
6
7
i64 sqlite3GetToken(const unsigned char *z, int *tokenType){
i64 i;
switch( aiClass[*z] ){
case CC_SPACE: {
for(i=1; sqlite3Isspace(z[i]); i++){}
*tokenType = TK_SPACE;
return i;
}
case CC_MINUS: {
if( z[1]=='-' ){
/* SQL 注释:-- 直到行尾 */
for(i=2; z[i]!=0 && z[i]!='\n'; i++){}
*tokenType = TK_COMMENT;
return i;
}
*tokenType = TK_MINUS;
return 1;
}
case CC_LT: {
if( z[1]=='=' ){
*tokenType = TK_LE; /* <= */
return 2;
}else if( z[1]=='>' ){
*tokenType = TK_NE; /* <> */
return 2;
}else{
*tokenType = TK_LT; /* < */
return 1;
}
}
/* ... 其余分类 ... */
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
为什么用查找表而不是直接 switch 字符值?源码注释给出了答案:
a switch() on aiClass[c] is implemented using a lookup table, whereas a switch() directly on c uses a binary search. The lookup table is much faster.
字符值的范围是 0--127,编译器可能生成二分查找的跳转表;而字符分类只有 30 种,编译器会生成直接索引的跳转表,快得多。这是 C 语言性能优化的典型技巧。
2.3 语法分析器
SQLite 的语法分析器使用 Lemon 解析器生成器——这是 SQLite 作者 D. Richard Hipp 自己写的工具。输入文件 parse.y 定义了 SQL 的 LALR(1) 文法,Lemon 将其编译为 parse.c。
Lemon 相比 yacc/bison 的关键区别:
- 不使用全局变量,所有状态通过参数传递(线程安全)。
- 解析器不负责 token 读取——外部代码调用
sqlite3Parser(pEngine, tokenType, token)逐个送入 token。 - 语法动作中直接构建内部数据结构(
Select、Expr、SrcList等),而非生成通用 AST。
tokenize.c 中的 sqlite3RunParser 函数将词法分析器和语法分析器串联起来:
int sqlite3RunParser(Parse *pParse, const char *zSql){
void *pEngine; /* Lemon 生成的解析器实例 */
int tokenType;
i64 n;
pEngine = sqlite3ParserAlloc(sqlite3Malloc, pParse);
while( 1 ){
n = sqlite3GetToken((u8*)zSql, &tokenType);
if( tokenType==TK_SPACE ){
zSql += n;
continue; /* 跳过空白 */
}
/* ... 处理注释、窗口函数关键字等特殊情况 ... */
sqlite3Parser(pEngine, tokenType, ...); /* 送入解析器 */
zSql += n;
if( /*解析完成或出错*/ ) break;
}
sqlite3ParserFree(pEngine, sqlite3_free);
return pParse->nErr;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
这是一个经典的词法驱动循环:反复调用 sqlite3GetToken 取出下一个 token,跳过空白和注释后送入 Lemon 解析器。
2.4 代码生成器
代码生成器是 SQLite 中最庞大的部分,分布在多个源文件中:
| 文件 | 职责 |
|---|---|
select.c | SELECT 语句的代码生成 |
where.c | WHERE 子句优化和代码生成 |
expr.c | 表达式求值的代码生成 |
build.c | CREATE TABLE、CREATE INDEX 等 DDL |
insert.c | INSERT 语句 |
update.c | UPDATE 语句 |
delete.c | DELETE 语句 |
代码生成器读取语法分析器构建的内部结构(Select、Expr 等),将其翻译为 VDBE 字节码指令序列。这一步还包括查询优化——where.c 负责选择最优的索引访问路径。
2.5 虚拟机 (VDBE)
VDBE(Virtual Database Engine)是 SQLite 的执行引擎。它是一个基于寄存器的字节码虚拟机,在概念上类似于 Java 的 JVM 或 Python 的解释器,但操作对象是数据库行和列而非通用数据。
每条编译好的 SQL 语句对应一个 Vdbe 结构体(在公开 API 中类型为 sqlite3_stmt *),其中存放着字节码程序和执行状态:
struct Vdbe {
sqlite3 *db; /* 所属的数据库连接 */
Vdbe **ppVPrev,*pVNext; /* 连接上所有 VDBE 的双向链表 */
Parse *pParse; /* 创建此 VDBE 的解析上下文 */
int nMem; /* 已分配的寄存器数量 */
int nCursor; /* 游标槽位数量 */
int pc; /* 程序计数器 */
int rc; /* 返回值 */
Op *aOp; /* 字节码指令数组 */
int nOp; /* 指令数量 */
Mem *aMem; /* 寄存器数组 */
VdbeCursor **apCsr; /* 游标数组 */
Mem *pResultRow; /* 当前输出行 */
/* ... */
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
用户每次调用 sqlite3_step() 时,VDBE 从 pc(程序计数器)处继续执行字节码,直到产生一行结果(返回 SQLITE_ROW)或执行完毕(返回 SQLITE_DONE)。
vdbe.c 的核心是一个巨大的 switch 循环,为每种操作码(opcode)实现一个 case 分支——操作码超过 180 种,包括 OP_OpenRead(打开游标)、OP_Column(读取列值)、OP_ResultRow(输出一行)等。
2.6 B-tree 层
B-tree 层(btree.c)管理数据库文件中的数据组织。SQLite 的每个表和索引都是一棵独立的 B-tree(更准确地说,表是 B+ tree,叶节点存储完整行数据;索引是 B-tree,内部节点也存储键值)。
B-tree 层向 VDBE 暴露游标接口——VDBE 通过游标在 B-tree 中定位、遍历和修改记录。游标的基本操作包括:
sqlite3BtreeMoveto:移动到指定键sqlite3BtreeNext/sqlite3BtreePrevious:移动到下一条 / 上一条sqlite3BtreeInsert:插入记录sqlite3BtreeDelete:删除记录
2.7 Pager 层
Pager(pager.c)是 B-tree 和磁盘之间的中间层,负责三件事:
- 页缓存:将磁盘页面缓存在内存中,减少 I/O。
- 事务管理:实现原子提交——要么全部写入成功,要么数据库文件不变。
- 崩溃恢复:通过日志(rollback journal 或 WAL)保证即使进程崩溃或断电,数据库也不会损坏。
SQLite 支持两种日志模式:传统的回滚日志(rollback journal)和写前日志(WAL,Write-Ahead Logging)。WAL 模式允许读写并发,是现代应用的推荐选择。
2.8 OS 接口 (VFS)
VFS(Virtual File System)是 SQLite 最底层的抽象,定义在 sqlite3_vfs 结构体中。它将所有与操作系统相关的操作——文件打开/关闭、读写、加锁、获取时间——封装在一组函数指针后面:
struct sqlite3_vfs {
int iVersion;
int szOsFile;
int mxPathname;
sqlite3_vfs *pNext;
const char *zName;
void *pAppData;
int (*xOpen)(sqlite3_vfs*, const char*, sqlite3_file*, int, int*);
int (*xDelete)(sqlite3_vfs*, const char*, int);
int (*xAccess)(sqlite3_vfs*, const char*, int, int*);
int (*xFullPathname)(sqlite3_vfs*, const char*, int, char*);
/* ... */
};2
3
4
5
6
7
8
9
10
11
12
13
SQLite 提供了 os_unix.c(POSIX 系统)和 os_win32.c(Windows)两套内置 VFS 实现。用户也可以注册自定义 VFS——例如将数据库存放在内存中、加密磁盘 I/O、或者在嵌入式系统上适配特殊的文件系统。
这种通过函数指针实现多态的模式是 C 语言中替代面向对象继承的标准做法。
3. 核心数据结构
sqlite3 连接句柄
sqlite3 结构体是整个数据库连接的中枢。它持有:连接的文件系统接口(pVfs)、所有活跃的虚拟机(pVdbe链表)、打开的数据库后端(aDb 数组,支持 ATTACH 多个数据库文件)、错误状态、以及各种运行时限制。
struct sqlite3 {
sqlite3_vfs *pVfs; /* 文件系统接口 */
struct Vdbe *pVdbe; /* 活跃虚拟机链表 */
Db *aDb; /* 所有数据库后端 */
int nDb; /* 当前使用的后端数量 */
u64 flags; /* 行为控制标志 */
i64 lastRowid; /* 最近一次 INSERT 的 ROWID */
int errCode; /* 最近的错误码 */
u8 autoCommit; /* 自动提交标志 */
u8 enc; /* 文本编码 (UTF-8/16) */
int nVdbeActive; /* 正在运行的 VDBE 数量 */
int aLimit[SQLITE_N_LIMIT]; /* 各种上限 */
/* ... 共约 90 个成员 ... */
};2
3
4
5
6
7
8
9
10
11
12
13
14
对比 kilo 编辑器的全局 editorConfig,SQLite 的 sqlite3 结构体也是"一个结构体装下所有状态"的模式,但它不是全局变量——它通过指针在函数间传递,允许一个进程同时打开多个独立的数据库连接。
除 sqlite3 外,另外两个频繁出现的核心结构体:
Vdbe(即sqlite3_stmt *):已编译的 SQL 语句。持有字节码程序、寄存器、游标、程序计数器。每次sqlite3_prepare_v2创建一个,sqlite3_finalize销毁一个。Parse:解析上下文。只在sqlite3_prepare_v2执行期间存在,作为词法分析器、语法分析器和代码生成器之间共享状态的载体。
struct Parse {
sqlite3 *db; /* 所属的数据库连接 */
Vdbe *pVdbe; /* 正在构建的虚拟机 */
int rc; /* 返回码 */
u8 nested; /* 嵌套调用深度 */
u8 isMultiWrite; /* 是否可能修改多行 */
/* ... */
};2
3
4
5
6
7
8
Parse 是典型的"上下文对象"模式——将多个阶段共享的状态打包在一个结构体中传递,而不是使用全局变量或过多的函数参数。
4. 一条查询的生命周期
prepare / step / finalize 三阶段生命周期
SQLite 将 SQL 语句的处理分为三个显式的阶段:
- Prepare(编译):
sqlite3_prepare_v2将 SQL 文本经过词法分析、语法分析、代码生成,编译为 VDBE 字节码程序,返回一个sqlite3_stmt *。 - Step(执行):
sqlite3_step驱动 VDBE 执行字节码。对于 SELECT,每次调用返回一行结果;对于 INSERT/UPDATE/DELETE,一次调用完成全部操作。 - Finalize(释放):
sqlite3_finalize释放编译后的语句占用的所有资源。
这个三阶段模型意味着同一条 SQL 可以被编译一次、执行多次(配合 sqlite3_reset 重置状态),避免重复解析和编译的开销。
用代码展示一个典型的使用流程:
sqlite3 *db;
sqlite3_stmt *stmt;
sqlite3_open("test.db", &db);
/* Prepare:编译 SQL → 字节码 */
sqlite3_prepare_v2(db,
"SELECT name FROM users WHERE id = ?;",
-1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, 42); /* 绑定参数 */
/* Step:逐行获取结果 */
while( sqlite3_step(stmt) == SQLITE_ROW ){
const char *name = (const char *)sqlite3_column_text(stmt, 0);
printf("name = %s\n", name);
}
/* Finalize:释放资源 */
sqlite3_finalize(stmt);
sqlite3_close(db);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
注意 sqlite3_bind_int 的用法——它将参数 ? 绑定到具体值。这既避免了 SQL 注入,又让同一条编译好的语句可以反复绑定不同参数执行。
5. 词法分析器详解
词法分析器是 SQLite 中最短、最独立的子系统之一(tokenize.c 约 900 行),适合作为源码阅读的起点。
5.1 字符分类系统
SQLite 不直接对字符值做 switch,而是先通过查找表 aiClass 将每个字节映射到一个 0--30 的"字符分类"编号。源码注释解释了原因:
a switch() on aiClass[c] is implemented using a lookup table, whereas a switch() directly on c uses a binary search. The lookup table is much faster.
aiClass 是一个 256 字节的静态数组,在编译时初始化。例如所有空白字符(空格、制表符、换行符)都映射到 CC_SPACE(7),所有数字映射到 CC_DIGIT(3)。这样 switch 分支只有 30 个 case,编译器可以生成跳转表而非二分搜索。
5.2 多字符运算符的处理
单字符 token(括号、逗号、分号等)的处理很直接——返回长度 1 和对应类型。但 SQL 有许多多字符运算符(<=、<>、!=、||、--、/*...*/),词法分析器用前瞻一个字符的方式处理:
case CC_LT: {
if( (c=z[1])=='=' ){
*tokenType = TK_LE; /* <= */
return 2;
}else if( c=='>' ){
*tokenType = TK_NE; /* <> */
return 2;
}else if( c=='<' ){
*tokenType = TK_LSHIFT; /* << */
return 2;
}else{
*tokenType = TK_LT; /* < */
return 1;
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
CC_MINUS 更有趣——减号 - 后面如果跟着另一个减号,就变成 SQL 注释 --:
case CC_MINUS: {
if( z[1]=='-' ){
for(i=2; (c=z[i])!=0 && c!='\n'; i++){}
*tokenType = TK_COMMENT;
return i; /* 注释延续到行尾 */
}
*tokenType = TK_MINUS;
return 1;
}2
3
4
5
6
7
8
9
5.3 词法分析器与解析器的交互
sqlite3RunParser 函数是词法分析器和语法分析器之间的桥梁。它的工作模式是一个简单的循环:
- 调用
sqlite3GetToken获取下一个 token 的类型和长度。 - 跳过空白和注释。
- 将 token 送入 Lemon 生成的解析器
sqlite3Parser。 - 重复直到 SQL 字符串耗尽或解析器报告完成/出错。
这种"外部驱动"的解析模式(由词法分析器循环向解析器喂 token)比 yacc 的"内部拉取"模式(解析器内部调用 yylex)更灵活——例如 SQLite 可以在喂入 token 之前做窗口函数关键字的上下文敏感判断。
6. 后续文章
本文给出了 SQLite 的鸟瞰图。后续文章将逐层深入:
- 虚拟机.md:深入 VDBE 字节码虚拟机——操作码体系、寄存器模型、
sqlite3_step的执行循环、用EXPLAIN查看字节码。 - B树与存储.md:深入存储层——B-tree 的页面结构、记录格式、Pager 的事务与崩溃恢复机制、WAL 模式的工作原理。
每篇文章都会从源码出发,展示 SQLite 是如何用纯 C 构建出生产级的数据库引擎的。