深入探讨 SQLite 存储

发布: (2026年1月15日 GMT+8 02:48)
9 min read
原文: Dev.to

Source: Dev.to

Hello, I’m Maneshwar. 我正在开发 FreeDevTools online —— 一个免费、开源的中心,汇集所有开发工具、技巧代码和 TL;DR,帮助开发者无需在网络上四处寻找。

昨天我们审视了 Page 1,即每个 SQLite 数据库文件的不可变起始点。
今天我们将深入文件中在 SQL 层面不可见,但对 SQLite 的空间管理、崩溃恢复和长期性能至关重要的部分。

本文将继续探讨 freelistsleaf pagestrunk pages,以及最终的 journals —— SQLite 的安全网。

为什么 SQLite 需要自由列表

  • SQLite 永不立即将未使用的页面返回给操作系统。
  • 一旦页面被分配,它会 保持 在文件中,直到执行显式的收缩操作。
  • 当行被删除、索引被删除或表被移除时,这些页面会变为 非活动

与其丢弃它们,SQLite 会将这些非活动页面放入一个称为 freelist 的结构中 —— 这是一份内部的未使用页面清单,可在将来插入时重复使用,从而避免文件增长。

什么是 Freelist?

Freelist 是 直接嵌入数据库文件内部的链式结构

Freelist 图示

关键事实

偏移量含义
32第一个 freelist 主干页编号(存储在文件头部)
36空闲页的总计数

所有空闲页都会被跟踪;没有垃圾回收或歧义。SQLite 将 freelist 组织为 类似根树的列表,从文件头部开始向外分支。

主干页和叶子页(空闲列表页)

空闲列表页有 两种子类型

主干页

主干页 充当空闲页的目录。它的布局(从页的起始位置开始)如下:

字节内容
4下一个主干页 的页号(如果没有则为 0
4此主干页上存储的叶子指针数量
N × 4叶子页的页号

每个主干页一次可以引用许多空闲页。

叶子页

叶子页 是不包含任何有意义结构的空闲页。其内容未定义,可能包含先前使用时遗留下来的数据。叶子页是真正可重用的页;主干页仅 指向它们

页面如何进入和离开空闲列表

  • 当页面变为不活跃时,SQLite 将其添加到空闲列表。该页面仍然物理存在于文件中。
  • 当需要写入新数据时,SQLite 从空闲列表中取出页面。

Freelist usage diagram

这解释了为什么 SQLite 数据库通常 会增长但不会自动缩小

缩小数据库:VACUUM 与 Autovacuum

如果空闲页列表(freelist)变得过大,磁盘使用会变得低效。SQLite 提供了两种解决方案。

VACUUM

VACUUM diagram

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

Autovacuum Mode

Autovacuum diagram

Autovacuum 通过少量的运行时开销换取持续的空间清理,会在空闲页面可用时自动将其移动到文件末尾。

SQLite 中的日志文件

日志(journal) 是一种崩溃恢复文件,用于记录数据库的更改,以便 SQLite 能够回滚未完成的事务。它保证了 原子性持久性,确保在故障后数据库不会出现半写入的状态。

SQLite 过去使用 传统日志(legacy journaling),包括:

  • 回滚日志(Rollback journal)
  • 语句日志(Statement journal)
  • 主日志(Master journal)

自 SQLite 3.7.0 起,数据库只能使用 传统日志或 WAL(写前日志),二者不会同时启用。内存数据库则完全不使用日志(日志仅存在于内存中)。

Journal types diagram

回滚日志:SQLite 的安全保障

每个数据库都有 一个回滚日志文件

  • 与数据库文件位于同一目录。
  • 文件名通过在数据库文件名后追加 -journal 获得。
  • 在写事务开始时创建。
  • 事务结束时(默认情况下)被删除。

回滚日志保存 数据库页面的前镜像(before‑images),从而在出现问题时能够恢复数据库。

回滚日志结构

(原始内容在此处结束;如果需要,可继续描述回滚日志的磁盘布局。)

Write‑Ahead Log (WAL) 日志概览

SQLite 日志被划分为 日志段

WAL segment illustration

每个段包含:

  • 段头
  • 一个或多个日志记录

大多数情况下只有 一个段;只有在特殊情况下才会出现多个段。

段头 – 第一道防线

每个段头以 八个魔术字节 开头:

D9 D5 05 F9 20 A1 63 D7

这些字节仅用于完整性检查。

Segment header fields

段头还存储:

  • 日志记录数量 (nRec)
  • 用于校验和计算的随机值
  • 原始数据库页数
  • 磁盘扇区大小
  • 数据库页大小

段头始终占用 恰好一个磁盘扇区,所有数值均采用 大端序 存储。

日志保留模式

默认情况下,SQLite 在提交或回滚后 删除 日志文件。您可以通过以下模式更改此行为:

模式描述
DELETE默认 – 每次事务结束后删除日志文件。
PERSIST保留日志文件,但在每次使用之间使其头部失效。
TRUNCATE每次事务结束后将日志文件截断为零长度。

独占锁模式 下,日志文件会跨事务保留,但其头部要么失效,要么在使用之间被截断。

异步事务(不安全但快速)

SQLite 支持一种 异步模式,以牺牲持久性来换取速度:

  • 日志和数据库文件 从不刷新 到磁盘。
  • 事务完成速度大幅提升。
  • nRec 被设为 ‑1
  • 恢复依赖文件大小而非元数据。

警告: 此模式 不具备崩溃安全性,仅应在开发或测试环境中使用,在性能提升大于数据丢失风险的情况下使用。

为什么这一层很重要

在这个低层次,SQLite 展示了其核心理念:

  • 空间被循环利用,而不是被丢弃。
  • 安全性 通过精确、最小化的元数据实现。
  • 没有隐式行为;所有内容都被显式跟踪。
  • 恢复逻辑 直接编码在文件结构中。

我关于 SQLite 的实验和动手示例可以在这里找到:
lovestaco/sqlite – SQLite examples

参考文献

  • SQLite Database System: Design and Implementation – Sibsankar Haldar(无日期)

FreeDevTools
👉 查看: FreeDevTools

欢迎任何反馈或贡献!该项目已上线,开源,随时可供任何人使用。

在 GitHub 上加星: HexmosTech/FreeDevTools

Back to Blog

相关文章

阅读更多 »

SQLite 中的日志:超越基础

介绍 你好,我是Maneshwar。我目前正在在线构建FreeDevTools——一个免费、开源的中心,汇集所有开发者工具、作弊码和TL;D...

SQLite 内部:文件命名

本章深入到 SQLite 的最低层——在这里,磁盘上的字节变成页面,页面形成树结构,且通过 journaling 来强制实现 durability……