深入探讨 SQLite 存储
Source: Dev.to
Hello, I’m Maneshwar. 我正在开发 FreeDevTools online —— 一个免费、开源的中心,汇集所有开发工具、技巧代码和 TL;DR,帮助开发者无需在网络上四处寻找。
昨天我们审视了 Page 1,即每个 SQLite 数据库文件的不可变起始点。
今天我们将深入文件中在 SQL 层面不可见,但对 SQLite 的空间管理、崩溃恢复和长期性能至关重要的部分。
本文将继续探讨 freelists、leaf pages、trunk pages,以及最终的 journals —— SQLite 的安全网。
为什么 SQLite 需要自由列表
- SQLite 永不立即将未使用的页面返回给操作系统。
- 一旦页面被分配,它会 保持 在文件中,直到执行显式的收缩操作。
- 当行被删除、索引被删除或表被移除时,这些页面会变为 非活动。
与其丢弃它们,SQLite 会将这些非活动页面放入一个称为 freelist 的结构中 —— 这是一份内部的未使用页面清单,可在将来插入时重复使用,从而避免文件增长。
什么是 Freelist?
Freelist 是 直接嵌入数据库文件内部的链式结构。

关键事实
| 偏移量 | 含义 |
|---|---|
| 32 | 第一个 freelist 主干页编号(存储在文件头部) |
| 36 | 空闲页的总计数 |
所有空闲页都会被跟踪;没有垃圾回收或歧义。SQLite 将 freelist 组织为 类似根树的列表,从文件头部开始向外分支。
主干页和叶子页(空闲列表页)
空闲列表页有 两种子类型。
主干页
主干页 充当空闲页的目录。它的布局(从页的起始位置开始)如下:
| 字节 | 内容 |
|---|---|
| 4 | 下一个主干页 的页号(如果没有则为 0) |
| 4 | 此主干页上存储的叶子指针数量 |
| N × 4 | 叶子页的页号 |
每个主干页一次可以引用许多空闲页。
叶子页
叶子页 是不包含任何有意义结构的空闲页。其内容未定义,可能包含先前使用时遗留下来的数据。叶子页是真正可重用的页;主干页仅 指向它们。
页面如何进入和离开空闲列表
- 当页面变为不活跃时,SQLite 将其添加到空闲列表。该页面仍然物理存在于文件中。
- 当需要写入新数据时,SQLite 从空闲列表中取出页面。

这解释了为什么 SQLite 数据库通常 会增长但不会自动缩小。
缩小数据库:VACUUM 与 Autovacuum
如果空闲页列表(freelist)变得过大,磁盘使用会变得低效。SQLite 提供了两种解决方案。
VACUUM

VACUUM 是一种重量级但精确的操作,会重建整个数据库文件,丢弃未使用的页面。
Autovacuum Mode

Autovacuum 通过少量的运行时开销换取持续的空间清理,会在空闲页面可用时自动将其移动到文件末尾。
SQLite 中的日志文件
日志(journal) 是一种崩溃恢复文件,用于记录数据库的更改,以便 SQLite 能够回滚未完成的事务。它保证了 原子性 和 持久性,确保在故障后数据库不会出现半写入的状态。
SQLite 过去使用 传统日志(legacy journaling),包括:
- 回滚日志(Rollback journal)
- 语句日志(Statement journal)
- 主日志(Master journal)
自 SQLite 3.7.0 起,数据库只能使用 传统日志或 WAL(写前日志),二者不会同时启用。内存数据库则完全不使用日志(日志仅存在于内存中)。

回滚日志:SQLite 的安全保障
每个数据库都有 一个回滚日志文件:
- 与数据库文件位于同一目录。
- 文件名通过在数据库文件名后追加
-journal获得。 - 在写事务开始时创建。
- 事务结束时(默认情况下)被删除。
回滚日志保存 数据库页面的前镜像(before‑images),从而在出现问题时能够恢复数据库。
回滚日志结构
(原始内容在此处结束;如果需要,可继续描述回滚日志的磁盘布局。)
Write‑Ahead Log (WAL) 日志概览
SQLite 日志被划分为 日志段。

每个段包含:
- 段头
- 一个或多个日志记录
大多数情况下只有 一个段;只有在特殊情况下才会出现多个段。
段头 – 第一道防线
每个段头以 八个魔术字节 开头:
D9 D5 05 F9 20 A1 63 D7
这些字节仅用于完整性检查。

段头还存储:
- 日志记录数量 (
nRec) - 用于校验和计算的随机值
- 原始数据库页数
- 磁盘扇区大小
- 数据库页大小
段头始终占用 恰好一个磁盘扇区,所有数值均采用 大端序 存储。
日志保留模式
默认情况下,SQLite 在提交或回滚后 删除 日志文件。您可以通过以下模式更改此行为:
| 模式 | 描述 |
|---|---|
DELETE | 默认 – 每次事务结束后删除日志文件。 |
PERSIST | 保留日志文件,但在每次使用之间使其头部失效。 |
TRUNCATE | 每次事务结束后将日志文件截断为零长度。 |
在 独占锁模式 下,日志文件会跨事务保留,但其头部要么失效,要么在使用之间被截断。
异步事务(不安全但快速)
SQLite 支持一种 异步模式,以牺牲持久性来换取速度:
- 日志和数据库文件 从不刷新 到磁盘。
- 事务完成速度大幅提升。
nRec被设为 ‑1。- 恢复依赖文件大小而非元数据。
警告: 此模式 不具备崩溃安全性,仅应在开发或测试环境中使用,在性能提升大于数据丢失风险的情况下使用。
为什么这一层很重要
在这个低层次,SQLite 展示了其核心理念:
- 空间被循环利用,而不是被丢弃。
- 安全性 通过精确、最小化的元数据实现。
- 没有隐式行为;所有内容都被显式跟踪。
- 恢复逻辑 直接编码在文件结构中。
我关于 SQLite 的实验和动手示例可以在这里找到:
lovestaco/sqlite – SQLite examples
参考文献
- SQLite Database System: Design and Implementation – Sibsankar Haldar(无日期)

👉 查看: FreeDevTools
欢迎任何反馈或贡献!该项目已上线,开源,随时可供任何人使用。
⭐ 在 GitHub 上加星: HexmosTech/FreeDevTools