跳到主要内容
浏览器
导航
数据库浏览器

SQLite 文件格式解析

SQLite 把整个数据库存放在单个文件里。本文从 100 字节文件头出发,说明页的划分方式、B 树页与溢出页的分工、单元格与空闲块的组织、sqlite_master 的元数据作用,以及回滚日志与 WAL 两种事务日志机制。

发布于 5 分钟阅读 1634 字

一个数据库引擎把数据、索引与元数据全部放进一个文件,意味着文件本身就构成一套可移植的存储格式。理解这种布局,有助于解释读写放大、文件体积增长与损坏恢复这几类行为。以下说明基于文件字节结构,具体字段偏移以官方格式文档为准。

页:文件的基本单位

文件被切分为等长的页,页是读写与缓存的基本单位。默认长度为 4096 字节,取值需为 512 到 65536 之间的 2 的幂。页从 1 开始编号,第 1 页的开头即文件头,文件头固定 100 字节,紧接其后的是第 1 页的 B 树页头。

偏移字段含义
0magic固定字符串,标识这是数据库文件
16page size每页字节数,取值 1 表示 65536
18 / 19write / read version读写版本号,取值 2 表示使用 WAL
28page count数据库总页数
32freelist trunk空闲页链表头所在页号
36freelist count空闲页总数
40schema cookie模式版本,模式变更时递增
52largest root page供空间回收流程使用

这些字段中,schema cookie 与客户端缓存关系较为直接:建表、加列或建索引都会改变它,连接据此判断已缓存的语句结构是否需要重新编译。freelist 两个字段描述的是一条空闲页链表,删除对象后释放的整页会挂到这条链上供后续分配复用。

# 读取头部中的页大小、总页数与空闲页数
sqlite3 app.db "PRAGMA page_size; PRAGMA page_count; PRAGMA freelist_count;"

# 以字节视图查看文件头,前 16 字节为格式标识字符串
xxd -l 100 app.db

页的几种角色

页的角色由该页 B 树页头部的 1 字节类型标识决定:

  • 表叶子页与表内部页
  • 索引叶子页与索引内部页
  • 溢出页
  • 空闲页,位于 freelist 链表上

B 树页头里记录了本页空闲块链表的首地址、单元格数量、单元格内容区的起始位置以及碎片字节数。内部页比叶子页多保存一个 4 字节的右侧子节点指针。表页与索引页的差别体现在单元格载荷上:表叶子页的单元格携带 rowid,索引叶子页的单元格则是键值与对应的 rowid。

第 1 页同时是 B 树根页,因此它的可用空间要从 100 字节文件头之后算起,这一点在计算页内容量时需要注意。

单元格与空闲块

单元格按页类型有不同的布局。以表叶子页为例,单元格依次为载荷长度(变长整数)、rowid(变长整数)与载荷本身。当载荷长度超过本页可容纳的上限时,超出的部分写入溢出页,原位置只保留一段指向溢出页链的指针,溢出页自身也以 4 字节指针串成链。

空闲块同样位于页内,头部记录下一空闲块的偏移与自身长度。单元格指针数组从页首向后增长,单元格内容区从页尾向前增长,两者相向而行,中间剩余的间隙即可分配空间。碎片字节数用于统计那些过小、无法再被复用的间隙。

sqlite_master 与元数据

第 1 页是 sqlite_master(新版实现中也称 sqlite_schema)的根页,它本身就是一张表,常见列为 type、name、tbl_name、rootpage 与 sql。每个表与索引在其中占一行,rootpage 指向该对象 B 树的根页。

查询一张表时,解析器先读这张模式表取出根页号,再进入对应的 B 树。也就是说,模式信息与其他表共用同一套页缓存与文件锁机制,并没有特殊的数据结构;相关的 B 树页组织方式可参见 B+ 树索引结构。

事务日志

改动数据库前先记录原状态,是实现原子性的通用做法。多数实现提供两种方式:

  • 回滚日志:修改页之前把原页内容写入带 journal 后缀的日志文件,提交时删除该文件。若进程崩溃,可用它把数据库文件恢复成事务开始前的状态。日志的写入顺序与文件系统的持久化语义相关,可参见 文件系统抽象层与虚拟列表。
  • WAL:改动追加写入带 wal 后缀的日志文件,内容由 24 字节帧头与整页数据组成的帧依次排列,另有共享内存文件保存帧索引。提交即写入一帧并更新帧头,读操作按各自可见的快照读取。

两种方式对读写并发的影响不同:回滚日志下写操作会阻塞读,而 WAL 允许读写并行,代价是引入了额外的日志文件与检查点流程。

页大小的取舍

页大小影响树的高度与每次读取的字节数,两者方向相反。页较大时,单个页能容纳的单元格更多,树的分支数增加、层数减少,一次查找需要读的页数随之减少;同时每页读入的字节数增加,对小行数据的空间利用率不利。页较小时,层数增加,但页内浪费减少。

页大小在数据库创建后不再变动(除非通过导出重建),因此通常在创建时按行的平均长度确定。它也会影响单条记录可用的载荷上限与溢出页出现的频率,进而影响一次查找涉及的页数。这一点与内存页缓存的命中率相互牵连:缓存能容纳的页数有限,页越大,缓存覆盖的记录数越多,但可缓存的页数越少。

小结

单文件数据库把页作为存储与缓存单位,用文件头描述全局状态,用 B 树页、溢出页与空闲页承担不同类型的内容,把模式信息也存成一张普通表。事务的原子性由日志机制维护,回滚日志与 WAL 在读写并发上各有取舍,而页大小在创建时就确定了树的形状与单次读取的成本。

数据库浏览器 · 相关文章