导读:本期聚焦于刘卫东创作的《SQLite数据库文件内部结构是怎样的?页、B树与记录格式详解》,敬请观看详情。想知道SQLite数据库文件里究竟存储了什么吗?这个单文件数据库并非一堆乱码,而是由固定大小的页精心组织而成。本文将解析SQLite文件的物理布局,从文件头的魔数与页大小,到B树页如何承载表数据与索引,再到记录头中的变长整数编码。理解这些底层结构有助于诊断数据库损坏、优化查询性能,甚至手动恢复数据。文中会给出十六进制查看示例和页头解析逻辑,帮你建立从字节到记录的完整认知。

SQLite数据库以其零配置、单文件存储的特点被广泛嵌入到各类应用中,但很少人真正打开过这个文件,看看里面的字节是如何排列的。实际上,SQLite文件并不是一个黑盒,它有一套严格定义的物理存储格式,所有表、索引、触发器和视图最终都落在一个个固定大小的页中。理解这种结构不仅能满足技术好奇心,还能在数据库文件损坏时帮助定位问题,或者为性能调优提供底层依据。接下来我们就从文件头开始,逐层拆解SQLite文件的内部构造。

SQLite数据库文件内部结构是怎样的?页、B树与记录格式详解

SQLite文件整体布局与页类型

SQLite数据库文件由一系列连续的页组成,每个页大小相同,默认是4096字节,也可以在创建数据库时通过PRAGMA page_size设置为512到65536之间的2的幂次。文件的第一页比较特殊,它的前100字节是文件头,用来描述整个数据库的元信息,后面跟着第一个B树页。从第二页开始,所有页都按用途划分为不同类型:B树页、空闲页、溢出页和锁页等。

文件头中最关键的信息包括魔数字符串“SQLite format 3\000”,它用来标识这是一个SQLite 3格式的数据库。紧随其后的两个字节表示页大小,但注意这个值存储的是以2为底的对数偏移,比如值为1表示实际页大小为512字节,值为2表示1024字节,通常看到的默认值4096字节对应存储值1。文件头还包含文件格式版本号、只读/读写标记、最大嵌入有效载荷分数、最小嵌入有效载荷分数和叶子有效载荷分数等,这些参数影响B树中溢出页的使用策略。

每种页都有一个一字节的页类型标记,位于页的第一个字节。常见的类型值包括:2表示内部索引B树页,5表示内部表B树页,10表示叶子索引B树页,13表示叶子表B树页。此外还有空闲页、溢出页和锁页。可以通过以下Python代码读取一个SQLite文件的前100字节,解析出魔数和页大小:

import struct

with open('test.db', 'rb') as f:
    header = f.read(100)

magic = header[0:16]
print("魔数:", magic)

page_size_raw = header[16:18]
page_size_value = struct.unpack('>H', page_size_raw)[0]
page_size = 1 if page_size_value == 1 else page_size_value * 256
print("页大小:", page_size)

# 读取文件格式版本
format_version = header[18:20]
print("文件格式版本:", struct.unpack('>H', format_version)[0])

这段代码会输出魔数“SQLite format 3\000”以及根据原始值转换后的实际页大小。理解文件头是后续解析页的基础,因为只有知道了页大小,才能正确跳转到任意页的偏移位置。

B树页的内部结构与记录存储

SQLite中的每张表和每个索引都对应一个B树,表使用B+树变体,索引也类似。B树页分为叶子页和内部页两种。对于一个表B树,叶子页存储实际的行记录,内部页存储键值和子页指针,键值通常是主键或rowid。对于索引B树,叶子页存储索引键和对应记录的rowid列表。

每个B树页开头都有一个页头,紧接着是单元格指针数组,最后是实际单元格内容。叶子表B树页的页头包含页类型、第一个空闲块偏移、单元格数量、单元格内容区起始偏移、碎片字节数和右兄弟页指针等字段。单元格指针数组是一个从页尾向前扩展的数组,每个指针指向一个单元格,这些单元格从页头之后的内容区开始向后增长,两者在页中间相遇即表示页满。

单元格中的记录格式是SQLite设计的精髓。一条记录由记录头和记录体组成,记录头连续存放每个列的serial type,serial type是一个变长整数,编码了值的数据类型和字节长度。例如serial type为1表示8位有符号整数,2表示16位整数,3表示24位整数,4表示32位整数,5表示48位整数,6表示64位整数,7表示IEEE浮点数,8表示整数0,9表示整数1,10和11用于布尔值,大于等于12且为偶数表示BLOB,长度为(serial type-12)/2,为奇数表示文本,长度为(serial type-13)/2。记录头以长度前缀作为一个整体存储,用户只需要解析出所有serial type并累加长度,就能定位到各列的值。

以下是一个简化的Python代码片段,用来解析一条SQLite记录的第一个serial type:

def read_varint(data, pos):
    result = 0
    shift = 0
    while True:
        byte = data[pos]
        pos += 1
        result |= (byte & 0x7F) << shift
        if (byte & 0x80) == 0:
            break
        shift += 7
    return result, pos

# 假设record是一条记录的字节串,record_header_size是记录头长度
record_header_size, pos = read_varint(record, 0)
serial_types = []
while pos < record_header_size:
    st, pos = read_varint(record, pos)
    serial_types.append(st)
print("各列serial type:", serial_types)

这段代码展示了变长整数的解码过程:每个字节的最高位是延续标志,低7位是数据。通过不断移位累加,直到遇到最高位为0的字节。这种编码方式对短整数非常高效,也解释了为什么SQLite在存储小整数时几乎不浪费空间。

空闲页管理与空间回收策略

当删除表中的行或者更新行导致大小变化时,SQLite并不会立即把释放的空间返还给操作系统,而是将这些页标记为空闲页,并加入空闲页链表中。空闲页链表由一个32位整数组成的链表头数组维护,这些指针存储在数据库文件的第1页偏移32处开始的4字节整数中,每个整数指向一个空闲页链表的头。链表中的每个空闲页的前4个字节存储下一个空闲页的页号,后4个字节存储链表中剩余空闲页的数量。

SQLite会在适当的时候复用这些空闲页,以避免文件无限增长。但频繁的增删操作也可能导致文件碎片化,即逻辑上连续的数据在物理上分散在不同页中,影响顺序扫描的性能。可以通过VACUUM命令重建数据库文件,消除空闲页并紧凑存储,但VACUUM需要额外的磁盘空间且会阻塞写操作。

在WAL(Write-Ahead Logging)模式下,SQLite的存储方式略有不同,主数据库文件仍然是页结构,但写操作先记录到独立的WAL文件中,只有检查点发生时才会将WAL中的页写回主文件。这种机制避免了阻塞读操作,但主文件中可能会存在一些已提交但尚未合并的页。理解空闲页和WAL机制对于分析数据库文件大小变化和恢复未提交事务非常有用。

总结来说,SQLite文件是一种自描述的二进制格式,通过页、B树和变长记录编码,在保持小巧灵活的同时提供了完整的ACID事务支持。无论你是想手动解析SQLite文件,还是优化嵌入式应用的存储性能,这些底层知识都会让你对SQLite有一个全新的认识。

SQLite文件结构页结构B树修改时间:2026-08-27 19:58:57

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。