你手机里几乎肯定躺着几十个 SQLite 数据库——Android 的通讯录、iOS 的备忘录、Chrome 的历史记录、微信的本地缓存,底层全是它。但它没有一个独立的服务进程,不需要安装、配置、调优,甚至整个数据库就是一个文件,可以直接 cp 备份、用 scp 传走、用邮件附件寄出去。The Programming Shuttle 这期《SQLite Internals: How The World's Most Used Database Works》要回答的就是一个看似平淡、实则极难的问题:凭什么一个"玩具般简单"的嵌入式数据库,能成为世界上部署量最大、也被认为最可靠的数据库之一?
答案藏在它四十年工程哲学凝结出的内核里:一套把 SQL 编译成字节码的虚拟机、一棵棵存储一切的 B-tree、一个手握日志与缓存大权的 Pager,以及一层薄到几乎不存在的 VFS。读懂这套内核,你不只是理解了一个数据库,而是理解了"可靠性"这件事在工程上到底是怎么被一行行代码"焊"出来的。
注:受当前网络环境对 YouTube 的访问限制(官方页、字幕接口、yt-dlp、公共前端均返回 bot 拦截),本解读未能逐字引用视频字幕,以下技术展开为基于视频所揭示主题、结合 SQLite 官方架构文档的延伸分析,已尽量标注出处;视频本身提供的是讲解框架与节奏。
| 项目 | 内容 |
|---|---|
| 视频标题 | SQLite Internals: How The World's Most Used Database Works |
| 频道 | The Programming Shuttle(@theprogrammingshuttle) |
| 时长 | 深度长视频(架构级讲解,具体以视频页为准) |
| 播放量 | 持续增长中(实时数据受平台访问限制未取,以视频页为准) |
| 核心主题 | SQLite 内部架构、B-tree 存储、Pager 与日志、WAL、可靠性工程 |
先说结论:这个称号不是营销,而是部署量撑起来的硬事实。每一台 Android 手机、每一台 iPhone、每一个主流桌面浏览器、每一架装了某些航电系统的飞机,几乎都在跑 SQLite。作者 D. Richard Hipp 在公开访谈里给出的量级是"万亿级(trillion)活跃数据库实例"——因为一台设备里可能同时跑着成百上千个独立的 .db 文件。
要做到这种渗透率,SQLite 的定位和 MySQL/PostgreSQL 完全不同:它不是"一个跑在服务器上的服务",而是一个被编译进你程序里的 C 库。没有客户端、没有网络协议、没有 DBA、没有端口要开。它的源码以 amalgamation(合并)形式发布——把整个数据库编译进一个 C 文件,编译产物仅数百 KB,直接 #include 进任何工程即可。更关键的是许可证:SQLite 是 Public Domain(公有领域),不是 MIT、不是 Apache——它根本没有版权约束,你可以拿去改、拿去卖、拿去塞进闭源商业产品,连署名都不用。这种"零摩擦"才是它能进每一台设备的真正原因。
延伸:Hipp 在 2000 年为一家做战舰系统的公司(通用动力旗下)写 SQLite,当时的需求就是"一个不需要 DBA、不会挂、能跟着产品走的数据库"。公司与 Hipp 约定放弃版权,SQLite 因此进入公有领域——这段历史直接决定了它今天的普及度。
SQLite 的内核被刻意切成清晰的两半——前端的"编译器"和后端的"虚拟机+存储"。一条 SELECT 从你敲下去到拿到结果,要走完下面这条流水线:
EXPLAIN 语句直接看到它。open/read/write/fsync)和锁。换一个 VFS 实现,SQLite 就能跑在另一个 OS、甚至跑在内存或自定义介质上。这个分层最大的好处是可移植性:上层逻辑(SQL→字节码→B-tree)完全不依赖操作系统,只有最底层的 VFS 需要针对平台重写。这就是为什么同一个 SQLite 能跑在 Linux 服务器、iOS 手表和飞机的黑匣子里。
很多人以为 SQLite 内部是某种"简化版"存储,其实它的存储结构相当正统:整个数据库就是一个文件,文件被切成固定大小的"页"(page,默认 4096 字节,可配 512–65536)。每一张表、每一个索引,都对应一棵独立的 B-tree。
这里有个容易被忽视的设计取舍:与 MySQL/PostgreSQL 的"堆表(heap table)+ 旁挂索引"不同,SQLite 的表本身就是一棵按主键排序的 B+tree——表数据物理上就聚簇在这棵树里。
rowid,表 B+tree 就按它排序。如果你的主键是 INTEGER PRIMARY KEY,它其实就是 rowid 的别名——这就是为什么整型主键在 SQLite 里又快又省空间。想绕过这套 B+tree 设计?可以用 WITHOUT ROWID 建表,让表直接按你指定的主键做聚簇 B-tree,省掉一层间接,对某些只按主键查询的场景更快。
如果说 B-tree 决定了"数据怎么摆",那 Pager 决定了"数据怎么活"。B-tree 层从不直接读写磁盘文件——它只跟 Pager 打交道,要一页就调 pager->fetch(pageNo),改完标记成脏页。Pager 在背后默默干着几件性命攸关的事:
理解 SQLite 的可靠性,90% 就是理解 Pager 在崩溃那一瞬间到底做了什么——也就是下面两节的内容。
这是 SQLite 最精妙、也是本期标题真正想点破的地方。在没有 WAL 之前,SQLite 靠的是 rollback journal(回滚日志) 实现原子提交,它用的是一条朴素但强悍的协议——改任何一页之前,先把这一页的原始内容备份到日志文件里:
fsync 日志文件,确保旧值确实落盘了。fsync。精妙之处在于崩溃恢复的判定极为简单:重启后若发现主库旁边没有 journal 文件 → 上次事务已提交,直接用;若发现 journal 还在 → 上次没提交完,把 journal 里的旧值拷回主库,事务被完整回滚。"日志文件存在与否"这一个事实,就编码了"事务是否完成"这一个布尔值。这是用最小的状态空间实现最大可靠性的经典范例。
代价是性能:每次提交要刷两次盘(日志一次、主库一次),而且写事务期间整个数据库被独占锁锁住——读写互斥。这正是 WAL 要解决的问题。
2010 年的 SQLite 3.7.0 引入了 WAL(Write-Ahead Logging) 模式,思路整个反过来:新数据不再覆盖主库,而是追加写进一个 -wal 文件。读者查询时,同时看主库和 wal,用一份共享内存(-shm,wal-index)快速定位"这一页最新有效版本在哪儿"。结果是:
fsync(刷 wal),不必动主库。对大多数读多写少的本地应用,开 WAL 几乎是"零成本提性能":一句 PRAGMA journal_mode=WAL; 搞定。唯一的坑是 WAL 引入了 -wal 和 -shm 两个伴随文件——备份时必须三个一起拷,否则会丢最新未 checkpoint 的写入。
SQLite 给自己的可靠性目标定得变态高:在任意时刻断电、杀进程、磁盘写一半,数据库都不能进入不可恢复状态。支撑这一点的不是某一行神级代码,而是一整套工程纪律:
.db 文件能用今天的 SQLite 打开。格式一旦定型就不再破坏性改动。这种"把可靠性焊死在每一层"的态度,正是为什么 SQLite 能进飞机、进医疗设备、进你手机里那些"绝对不能丢"的数据。可靠性不是功能,是默认值。
flowchart TD
A["SQL 文本<br/>SELECT ... FROM ..."] --> B["Tokenizer / Parser<br/>词法 + 语法 → AST"]
B --> C["Code Generator<br/>AST → 字节码"]
C --> D["VDBE 虚拟机<br/>基于寄存器,逐条解释执行<br/>可用 EXPLAIN 查看"]
D --> E["B-tree 层<br/>表=B+tree / 索引=B-tree<br/>按 rowid 排序聚簇"]
E --> F["Pager 页管理器<br/>页缓存 · 脏页 · 日志 · 崩溃恢复<br/>事务的真正掌权者"]
F --> G["VFS 操作系统接口<br/>open / read / write / fsync · 锁"]
G --> H[("磁盘:单个 .db 文件<br/>+ journal / -wal / -shm")]
F -. "改页前先备份旧值" .-> J["Rollback Journal<br/>原子提交两段式"]
F -. "新值追加写入" .-> W["WAL 模式<br/>读写不互锁"]
style D fill:#fef3c7,stroke:#b45309
style F fill:#dbeafe,stroke:#1d4ed8
style J fill:#fee2e2,stroke:#b91c1c
style W fill:#dcfce7,stroke:#15803d
1. 把 SQLite 当"应用默认本地存储",而不是 Last Resort。 很多人习惯性地给应用套一个 Postgres/MySQL,其实 90% 的单机应用用 SQLite 更好:零运维、零网络往返(进程内调用是纳秒级)、备份就是拷文件。本地优先(local-first)软件的复兴——Figma 的多端协作、各种笔记/设计工具——背后大量是 SQLite 或它的变体。
2. 上线第一件事:开 WAL。 PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; 这两条配置对绝大多数读多写少的场景是性能与安全的甜点。默认的 DELETE 模式适合只读或极低写入的场景,WAL 才是通用默认。
3. 理解写入的真正代价是 fsync,不是 SQL。 一个事务的耗时几乎等于 fsync 的次数。批量写要合并到一个事务里(BEGIN ... 多条 ... COMMIT),而不是每条自动提交——后者可能慢上几百倍。
4. 并发写的天花板要心里有数。 即使是 WAL,也只允许一个 writer。高并发写入(如多人实时协同、高 QPS 日志写入)是 SQLite 的短板,这时才该上 Postgres/MySQL 或分布式 SQLite 方案(Litestream、Turso/libSQL、dqlite、rqlite)。
5. 备份别只拷主文件。 WAL 模式下 .db/.wal/.shm 三件套必须一起备份;或者更稳的方式是用 .backup 命令在线生成一个一致快照,再拷走。
6. 可靠性可以"抄作业"。 SQLite 用"先写日志再改主数据"实现原子性、用"删除日志=提交点"编码事务状态、用 MC/DC 100% 覆盖保证质量——这些模式完全可以迁移到你自己的关键系统设计里:凡是涉及"改状态又要防崩溃"的地方,先想想"我的 journal 在哪、我的 commit point 在哪"。
.db 文件、或理解 B-tree 页布局,看这一篇。SQLite 把"可靠性"拆解成了四层架构 + 一个"先写日志后改库"的原子协议,让一个几百 KB 的单文件库,成了装在万亿设备上、断电也不坏的工程奇迹——读懂它的 Pager 与日志,你就读懂了所有数据库追求的那件事:在崩溃面前,让数据永远自洽。
本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。
youtube_transcript.py 补抓字幕,对"视频原话 vs 延伸"做逐句校准(优先级:中)。[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
