本文档描述并定义了自 3.0.0 版(2004-06-18)以来所有 SQLite 版本使用的磁盘上数据库文件格式。
SQLite 数据库的完整状态通常包含在磁盘上的单个文件中,称为“主数据库文件”。
在事务期间,SQLite 将其他信息存储在第二个文件中,称为“回滚日志”,或者如果 SQLite 处于 WAL 模式,则存储在预写日志文件中。
如果应用程序或主机计算机在事务完成之前崩溃,则回滚日志或预写日志包含恢复主数据库文件到一致状态所需的信息。当回滚日志或预写日志包含恢复数据库状态所需的信息时,它们被称为“热日志”或“热 WAL 文件”。热日志和 WAL 文件仅在错误恢复场景中起作用,因此并不常见,但它们是 SQLite 数据库状态的一部分,因此不能忽略。本文档定义了回滚日志和预写日志文件的格式,但重点是主数据库文件。
主数据库文件由一个或多个页面组成。页面的大小是 512 到 65536(含)之间的 2 的幂。同一数据库中的所有页面大小相同。数据库文件的文件大小由数据库文件开头偏移 16 字节处的 2 字节整数确定。
页面的编号从 1 开始。最大页面编号为 4294967294(232 - 2)。SQLite 数据库的最小大小为单个 512 字节页面。最大大小的数据库将是 4294967294 个页面,每个页面 65536 字节,或 281,474,976,579,584 字节(约 281 TB)。通常,SQLite 会在达到其自身的内部大小限制之前很久就达到底层文件系统或磁盘硬件的最大文件大小限制。
在常见用法中,SQLite 数据库的大小范围从几 KB 到几 GB 不等,尽管已知在生产环境中存在 TB 级别的 SQLite 数据库。
在任何时间点,主数据库中的每个页面都具有单一用途,该用途是以下用途之一
对主数据库文件的所有读取和写入都从页面边界开始,并且所有写入的大小都是页面大小的整数倍。读取通常也是页面大小的整数倍,唯一的例外是当数据库第一次打开时,数据库文件的前 100 字节(数据库文件头)以子页面大小的单位读取。
数据库文件的前 100 字节构成数据库文件头。数据库文件头被划分为字段,如下表所示。数据库文件头中的所有多字节字段都以最高有效字节优先(大端)存储。
偏移量 | 大小 | 描述 |
---|---|---|
0 | 16 | 头字符串:“SQLite format 3\000” |
16 | 2 | 数据库页面大小(以字节为单位)。必须是 512 到 32768(含)之间的 2 的幂,或者值为 1 表示页面大小为 65536。 |
18 | 1 | 文件格式写入版本。1 表示传统版本;2 表示 WAL。 |
19 | 1 | 文件格式读取版本。1 表示传统版本;2 表示 WAL。 |
20 | 1 | 每个页面末尾未使用的“保留”空间的字节数。通常为 0。 |
21 | 1 | 最大嵌入式数据部分比例。必须为 64。 |
22 | 1 | 最小嵌入式数据部分比例。必须为 32。 |
23 | 1 | 叶节点数据部分比例。必须为 32。 |
24 | 4 | 文件变更计数器。 |
28 | 4 | 数据库文件的大小(以页面为单位)。“头部记录的数据库大小”。 |
32 | 4 | 第一个空闲列表主干页面的页面号。 |
36 | 4 | 空闲列表页面的总数。 |
40 | 4 | 模式 Cookie。 |
44 | 4 | 模式格式编号。支持的模式格式为 1、2、3 和 4。 |
48 | 4 | 默认页面缓存大小。 |
52 | 4 | 在自动真空或增量真空模式下,最大的根 B 树页面的页面号,否则为零。 |
56 | 4 | 数据库文本编码。值为 1 表示 UTF-8。值为 2 表示 UTF-16le。值为 3 表示 UTF-16be。 |
60 | 4 | 由 user_version pragma 读取和设置的“用户版本”。 |
64 | 4 | 增量真空模式为真(非零)。否则为假(零)。 |
68 | 4 | 由 PRAGMA application_id 设置的“应用程序 ID”。 |
72 | 20 | 预留以供扩展。必须为零。 |
92 | 4 | 版本有效期编号。 |
96 | 4 | SQLITE_VERSION_NUMBER |
每个有效的 SQLite 数据库文件都以以下 16 个字节开头(以十六进制表示):53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00。此字节序列对应于 UTF-8 字符串“SQLite format 3”,包括末尾的空终止符字符。
从偏移量 16 开始的两个字节值确定数据库的页面大小。对于 3.7.0.1 版(2010-08-04)及更早版本的 SQLite,此值被解释为大端整数,并且必须是 512 到 32768(含)之间的 2 的幂。从 SQLite 3.7.1 版(2010-08-23)开始,支持 65536 字节的页面大小。值 65536 无法放入两个字节的整数中,因此要指定 65536 字节的页面大小,偏移量 16 处的值为 0x00 0x01。此值可以解释为大端 1,并被认为是表示 65536 页面大小的魔数。或者,可以将这两个字节字段视为小端数字,并说它表示页面大小除以 256。页面大小字段的这两种解释是等效的。
偏移量 18 和 19 处的文件格式写入版本和文件格式读取版本旨在允许在未来版本的 SQLite 中增强文件格式。在当前版本的 SQLite 中,这两个值对于回滚日志模式为 1,对于 WAL 日志模式为 2。如果根据当前文件格式规范编码的 SQLite 版本遇到数据库文件,其中读取版本为 1 或 2 但写入版本大于 2,则必须将数据库文件视为只读。如果遇到读取版本大于 2 的数据库文件,则无法读取或写入该数据库。
SQLite 能够在每个页面的末尾预留少量额外的字节,供扩展使用。例如,SQLite 加密扩展使用这些额外字节来存储与每个页面关联的 nonce 和/或加密校验和。偏移量 20 处的 1 字节整数中的“保留空间”大小是每个页面末尾为扩展保留的空间字节数。此值通常为 0。该值可以是奇数。
数据库页面的“可用大小”是页眉中偏移量 16 处的 2 字节整数指定的页面大小减去页眉中偏移量 20 处的 1 字节整数中记录的“保留”空间大小。页面的可用大小可能是奇数。但是,可用大小不允许小于 480。换句话说,如果页面大小为 512,则保留空间大小不能超过 32。
最大和最小嵌入式数据部分比例以及叶节点数据部分比例值必须分别为 64、32 和 32。这些值最初旨在成为可调整的参数,可用于修改 B 树算法的存储格式。但是,此功能不受支持,并且目前没有计划在将来添加支持。因此,这三个字节固定为指定的值。
文件变更计数器是偏移量 24 处的 4 字节大端整数,每当数据库文件在修改后解锁时都会递增。当两个或多个进程读取同一个数据库文件时,每个进程都可以通过监视变更计数器来检测来自其他进程的数据库更改。当另一个进程修改数据库时,进程通常希望刷新其数据库页面缓存,因为缓存已过期。文件变更计数器有助于实现这一点。
在 WAL 模式下,使用 wal 索引检测对数据库的更改,因此不需要变更计数器。因此,在 WAL 模式下,变更计数器可能不会在每个事务中递增。
头部偏移量 28 处的 4 字节大端整数存储数据库文件的大小(以页面为单位)。如果此头部记录的数据库大小无效(请参阅下一段),则通过查看数据库文件的实际大小来计算数据库大小。较旧版本的 SQLite 忽略了头部记录的数据库大小,而仅使用实际文件大小。较新版本的 SQLite 会使用头部记录的数据库大小(如果可用),但如果头部记录的数据库大小无效,则会回退到实际文件大小。
只有当页眉数据库大小非零且偏移量为 24 处的 4 字节更改计数器完全匹配偏移量为 92 处的 4 字节版本有效期编号时,才认为页眉数据库大小有效。当数据库仅使用 SQLite 的最新版本(3.7.0(2010-07-21)及更高版本)修改时,页眉数据库大小始终有效。如果旧版本的 SQLite 写入数据库,它将不知道更新页眉数据库大小,因此页眉数据库大小可能不正确。但旧版本的 SQLite 也会使偏移量为 92 处的版本有效期编号保持不变,因此它将不匹配更改计数器。因此,可以通过观察更改计数器与版本有效期编号不匹配的情况来检测(并忽略)无效的页眉数据库大小。
数据库文件中未使用的页面存储在空闲列表中。偏移量为 32 处的 4 字节大端整数存储空闲列表第一页的页码,如果空闲列表为空则为零。偏移量为 36 处的 4 字节大端整数存储空闲列表中页面的总数。
模式 Cookie 是偏移量为 40 处的 4 字节大端整数,每当数据库模式更改时都会递增。预编译语句针对数据库模式的特定版本进行编译。当数据库模式更改时,必须重新编译语句。当预编译语句运行时,它首先检查模式 Cookie 以确保其值与编译语句时相同,如果模式 Cookie 已更改,则语句会自动重新编译并重新运行,或者以SQLITE_SCHEMA错误中止。
模式格式编号是偏移量为 44 处的 4 字节大端整数。模式格式编号类似于偏移量为 18 和 19 处的文件格式读写版本号,但模式格式编号指的是高级 SQL 格式,而不是低级 B 树格式。目前定义了四个模式格式编号。
SQLite 创建的新数据库文件默认使用格式 4。legacy_file_format pragma 可用于使 SQLite 使用格式 1 创建新的数据库文件。可以通过在编译时设置SQLITE_DEFAULT_FILE_FORMAT=1,使格式版本号默认为 1 而不是 4。
如果数据库完全为空,即没有模式,则模式格式编号可以为零。
偏移量为 48 处的 4 字节大端有符号整数是数据库文件的建议缓存大小(以页为单位)。该值仅为建议,SQLite 并不承担必须遵守它的义务。该整数的绝对值用作建议大小。可以使用default_cache_size pragma设置建议的缓存大小。
偏移量为 52 和 64 处的两个 4 字节大端整数用于管理auto_vacuum和incremental_vacuum模式。如果偏移量为 52 处的整数为零,则指针映射 (ptrmap) 页面将从数据库文件中省略,并且不支持 auto_vacuum 或 incremental_vacuum。如果偏移量为 52 处的整数非零,则它是数据库文件中最大根页面的页码,数据库文件将包含 ptrmap 页面,并且模式必须为 auto_vacuum 或 incremental_vacuum。在这种情况下,偏移量为 64 处的整数对于 incremental_vacuum 为真,对于 auto_vacuum 为假。如果偏移量为 52 处的整数为零,则偏移量为 64 处的整数也必须为零。
偏移量为 56 处的 4 字节大端整数确定用于存储在数据库中的所有文本字符串的编码。值为 1 表示 UTF-8。值为 2 表示 UTF-16le。值为 3 表示 UTF-16be。不允许使用其他值。sqlite3.h 头文件定义了 C 预处理器宏 SQLITE_UTF8 为 1,SQLITE_UTF16LE 为 2,SQLITE_UTF16BE 为 3,以代替文本编码的数字代码。
偏移量为 60 处的 4 字节大端整数是用户版本,由user_version pragma设置和查询。SQLite 不使用用户版本。
偏移量为 68 处的 4 字节大端整数是“应用程序 ID”,可以通过PRAGMA application_id命令设置,以识别数据库属于或与特定应用程序关联。应用程序 ID 适用于用作应用程序文件格式的数据库文件。应用程序 ID 可以被诸如file(1)之类的实用程序用来确定特定的文件类型,而不仅仅是报告“SQLite3 数据库”。可以通过查阅 SQLite 源代码存储库中的magic.txt文件查看已分配的应用程序 ID 列表。
偏移量为 96 处的 4 字节大端整数存储最近修改数据库文件的 SQLite 库的SQLITE_VERSION_NUMBER值。偏移量为 92 处的 4 字节大端整数是在存储版本号时更改计数器的值。偏移量为 92 处的整数指示版本号对哪个事务有效,有时称为“版本有效期编号”。
数据库文件页眉的所有其他字节都保留用于将来的扩展,必须设置为零。
锁字节页面是数据库文件中的单个页面,包含偏移量在 1073741824 到 1073742335(含)之间的字节。大小小于或等于 1073741824 字节的数据库文件不包含锁字节页面。大于 1073741824 字节的数据库文件正好包含一个锁字节页面。
锁字节页面留作操作系统特定的VFS实现用于实现数据库文件锁定原语。SQLite 不使用锁字节页面。SQLite 内核永远不会读取或写入锁字节页面,尽管操作系统特定的VFS实现可能会根据底层系统的需求和倾向选择读取或写入锁字节页面上的字节。内置于 SQLite 中的 unix 和 win32 VFS实现不会写入锁字节页面,但其他操作系统的第三方 VFS 实现可能会写入。
锁字节页面的出现是为了支持 Win95,这是设计此文件格式时的主要操作系统,并且仅支持强制文件锁定。我们所知的所有现代操作系统都支持建议性文件锁定,因此锁字节页面实际上不再需要,但为了向后兼容而保留。
数据库文件可能包含一个或多个未处于活动状态的页面。例如,当从数据库中删除信息时,可能会出现未使用的页面。未使用的页面存储在空闲列表中,并在需要更多页面时重新使用。
空闲列表被组织成一个空闲列表主干页面的链接列表,每个主干页面包含零个或多个空闲列表叶子页面的页码。
空闲列表主干页面由一个 4 字节大端整数数组组成。数组的大小与页面可用空间中可以容纳的整数数量相同。最小可用空间为 480 字节,因此数组的长度至少为 120 个条目。空闲列表主干页面上的第一个整数是列表中下一个空闲列表主干页面的页码,如果这是最后一个空闲列表主干页面,则为零。空闲列表主干页面上的第二个整数是要跟随的叶子页面指针数。将空闲列表主干页面上的第二个整数称为 L。如果 L 大于零,则数组索引在 2 到 L+1(含)之间的整数包含空闲列表叶子页面的页码。
空闲列表叶子页面不包含任何信息。SQLite 避免读取或写入空闲列表叶子页面以减少磁盘 I/O。
SQLite 3.6.0(2008-07-16)之前的版本中的一个错误导致如果空闲列表主干页面数组中的最后 6 个条目包含非零值,则数据库会被报告为损坏。较新版本的 SQLite 没有此问题。但是,较新版本的 SQLite 仍然避免使用空闲列表主干页面数组中的最后六个条目,以便较新版本的 SQLite 创建的数据库文件可以被旧版本的 SQLite 读取。
空闲列表页面的数量存储为数据库页眉中的 4 字节大端整数,该整数位于文件开头偏移量为 36 的位置。数据库页眉还将第一个空闲列表主干页面的页码存储为 4 字节大端整数,该整数位于文件开头偏移量为 32 的位置。
B 树算法在面向页面的存储设备上提供具有唯一且有序键的键/数据存储。有关 B 树的背景信息,请参阅 Knuth 的《计算机程序设计艺术》,第 3 卷“排序和搜索”,第 471-479 页。SQLite 使用两种 B 树变体。“表 B 树”使用 64 位有符号整数键,并将所有数据存储在叶子中。“索引 B 树”使用任意键,并且根本不存储任何数据。
B 树页面可以是内部页面或叶子页面。叶子页面包含键,并且在表 B 树的情况下,每个键都有关联的数据。内部页面包含 K 个键以及 K+1 个指向子 B 树页面的指针。内部 B 树页面中的“指针”只是子页面的 32 位无符号整数页码。
内部 B 树页面的键数量 K 几乎总是至少为 2,通常远大于 2。唯一的例外是页面 1 是内部 B 树页面时。由于数据库头位于该页面的开头,页面 1 可用的存储空间减少了 100 字节,因此有时(很少)如果页面 1 是内部 B 树页面,它最终可能只包含一个键。在所有其他情况下,K 为 2 或更大。K 的上限是页面上可以容纳的键的数量。索引 B 树上的大键被分成溢出页,以便没有单个键使用页面上超过四分之一的可用存储空间,因此每个内部页面都能够存储至少 4 个键。表 B 树的整数键永远不会大到需要溢出,因此键溢出仅发生在索引 B 树上。
将叶 B 树的深度定义为 1,将任何内部 B 树的深度定义为其任何子节点的最大深度加 1。在格式良好的数据库中,内部 B 树的所有子节点都具有相同的深度。
在内部 B 树页面中,指针和键在逻辑上交替出现,两端都有一个指针。(这句话应该从概念上理解——键和指针在页面内的实际布局更加复杂,将在后续描述。)同一页面内的所有键都是唯一的,并且在逻辑上从左到右按升序排列。(同样,此排序是逻辑上的,而不是物理上的。键在页面内的实际位置是任意的。)对于任何键 X,X 左侧的指针引用所有键都小于或等于 X 的 B 树页面。X 右侧的指针引用所有键都大于 X 的页面。
在内部 B 树页面内,每个键及其左侧的直接指针组合成一个称为“单元格”的结构。最右边的指针单独保存。叶 B 树页面没有指针,但它仍然使用单元格结构来保存索引 B 树的键或表 B 树的键和内容。数据也包含在单元格中。
每个 B 树页面最多只有一个父 B 树页面。没有父级的 B 树页面称为根页面。根 B 树页面及其子节点的闭包一起形成一个完整的 B 树。一个完整的 B 树可能(实际上相当常见)只包含一个既是叶节点又是根节点的页面。因为从父节点到子节点有指针,所以如果只知道根页面,就可以找到完整 B 树的每个页面。因此,B 树由其根页面号标识。
B 树页面要么是表 B 树页面,要么是索引 B 树页面。每个完整 B 树内的所有页面都属于同一类型:表或索引。数据库文件中每个数据库模式中的 rowid 表(包括sqlite_schema等系统表)都有一个表 B 树。数据库文件中每个模式中的每个索引(包括唯一性约束创建的隐式索引)都有一个索引 B 树。虚拟表没有关联的 B 树。特定的虚拟表实现可能会利用影子表进行存储,但这些影子表将在数据库模式中具有单独的条目。WITHOUT ROWID表使用索引 B 树而不是表 B 树,因此数据库文件中每个WITHOUT ROWID表都有一个索引 B 树。对应于 sqlite_schema 表的 B 树始终是表 B 树,并且始终具有 1 的根页面。sqlite_schema 表包含数据库文件中每个其他表和索引的根页面号。
表 B 树中的每个条目由一个 64 位有符号整数键和最多 2147483647 字节的任意数据组成。(表 B 树的键对应于 B 树实现的 SQL 表的rowid。)内部表 B 树仅保存键和指向子节点的指针。所有数据都包含在表 B 树的叶子中。
索引 B 树中的每个条目由一个长度最多为 2147483647 字节的任意键组成,并且没有数据。
将单元格的“有效负载”定义为单元格的任意长度部分。对于索引 B 树,键的长度始终是任意的,因此有效负载是键。内部表 B 树页面的单元格中没有任意长度的元素,因此这些单元格没有有效负载。表 B 树叶页面包含任意长度的内容,因此对于这些页面上的单元格,有效负载是内容。
当单元格的有效负载大小超过某个阈值(稍后定义)时,只有有效负载的前几个字节存储在 B 树页面上,其余部分存储在内容溢出页面的链接列表中。
B 树页面按以下顺序划分为区域
100 字节的数据库文件头仅在页面 1 上找到,页面 1 始终是表 B 树页面。数据库文件中的所有其他 B 树页面都省略了这 100 字节的头。
保留区域是每个页面(锁定页面除外)末尾的未使用空间区域,扩展程序可以使用它来保存每个页面的信息。保留区域的大小由数据库文件头偏移量 20 处的 1 字节无符号整数确定。保留区域的大小通常为零。
叶页面的 B 树页面头大小为 8 字节,内部页面的大小为 12 字节。页面头中的所有多字节值都是大端序。B 树页面头由以下字段组成
偏移量 | 大小 | 描述 |
---|---|---|
0 | 1 | 偏移量 0 处的 1 字节标志,指示 B 树页面类型。
|
1 | 2 | 偏移量 1 处的 2 字节整数给出页面上第一个空闲块的起始位置,如果不存在空闲块,则为零。 |
3 | 2 | 偏移量 3 处的 2 字节整数给出页面上的单元格数量。 |
5 | 2 | 偏移量 5 处的 2 字节整数指定单元格内容区域的起始位置。此整数的零值被解释为 65536。 |
7 | 1 | 偏移量 7 处的 1 字节整数给出单元格内容区域内碎片空闲字节的数量。 |
8 | 4 | 偏移量 8 处的 4 字节页面号是最右边的指针。此值仅出现在内部 B 树页面的头中,并在所有其他页面中省略。 |
B 树页面的单元格指针数组紧跟在 B 树页面头之后。令 K 为 B 树上的单元格数量。单元格指针数组由 K 个 2 字节整数偏移量组成,指向单元格内容。单元格指针按键顺序排列,最左边的单元格(键最小的单元格)排在最前面,最右边的单元格(键最大的单元格)排在最后面。
单元格内容存储在 B 树页面的单元格内容区域中。SQLite 努力将单元格尽可能地放置在 B 树页面的末尾,以便为单元格指针数组的未来增长预留空间。最后一个单元格指针数组条目和第一个单元格的开始之间的区域是未分配区域。
如果页面不包含任何单元格(这只有在不包含任何行的表的根页面上才可能),则单元格内容区域的偏移量将等于页面大小减去保留空间的字节数。如果数据库使用 65536 字节的页面大小,并且保留空间为零(保留空间的常用值),则空页面的单元格内容偏移量应为 65536。但是,该整数太大,无法存储在 2 字节无符号整数中,因此使用 0 值代替。
空闲块是用于识别 B 树页面内未分配空间的结构。空闲块被组织成一个链。空闲块的前 2 个字节是一个大端序整数,表示链中下一个空闲块在 B 树页面中的偏移量,如果空闲块是链中的最后一个,则为零。每个空闲块的第三和第四个字节形成一个大端序整数,表示空闲块的大小(以字节为单位),包括 4 字节的头。空闲块始终按偏移量递增的顺序连接。B 树页面头的第二个字段是第一个空闲块的偏移量,如果页面上没有空闲块,则为零。在格式良好的 B 树页面中,第一个空闲块之前始终至少有一个单元格。
空闲块至少需要 4 字节的空间。如果单元格内容区域内存在 1、2 或 3 个孤立的未使用字节组,则这些字节构成一个碎片。所有碎片中的字节总数存储在 B 树页面头的第五个字段中。在格式良好的 B 树页面中,碎片中的字节总数不得超过 60。
B 树页面上的总空闲空间由未分配区域的大小加上所有空闲块的总大小加上碎片空闲字节的数量组成。SQLite 可能会不时地重新组织 B 树页面,以便没有空闲块或碎片字节,所有未使用字节都包含在未分配空间区域中,并且所有单元格都紧密地打包在页面的末尾。这称为“碎片整理”B 树页面。
可变长度整数或“varint”是对 64 位二进制补码整数的静态霍夫曼编码,对小的正值使用更少的空间。varint 的长度在 1 到 9 字节之间。varint 由零个或多个高位设置为 1 的字节加上一个高位设置为 0 的字节组成,或者 9 个字节,取两者中较短者。前 8 个字节中的每个字节的低 7 位和第 9 个字节的所有 8 位都用于重构 64 位二进制补码整数。Varint 是大端序:从 varint 的较早字节中获取的位比从较晚字节中获取的位更重要。
单元格的格式取决于单元格出现在哪种 B 树页面上。下表按顺序显示了各种 B 树页面类型的单元格元素。
表 B 树叶单元格(头 0x0d)
表 B 树内部单元格(头 0x05)
索引 B 树叶单元格(头 0x0a)
索引 B 树内部单元格(头部 0x02)
以上信息可以重新整理成表格格式,如下所示
数据类型 | 出现在… | 描述 | |||
---|---|---|---|---|---|
表叶子节点 (0x0d) | 表内部节点 (0x05) | 索引叶子节点 (0x0a) | 索引内部节点 (0x02) | ||
4 字节整数 | ✔ | ✔ | 左子节点的页码 | ||
变长整数 | ✔ | ✔ | ✔ | 有效负载的字节数 | |
变长整数 | ✔ | ✔ | 行 ID | ||
字节数组 | ✔ | ✔ | ✔ | 有效负载 | |
4 字节整数 | ✔ | ✔ | ✔ | 第一个溢出页的页码 |
溢出到溢出页的有效负载量也取决于页类型。对于以下计算,令 U 为数据库页的可使用大小,即总页大小减去每个页末尾的保留空间。并令 P 为有效负载大小。在以下内容中,符号 X 表示可以在 B 树页上直接存储的最大有效负载量,而无需溢出到溢出页,符号 M 表示在允许溢出之前必须存储在 B 树页上的最小有效负载量。
表 B 树叶子节点单元格
令 X 为 U-35。如果有效负载大小 P 小于或等于 X,则整个有效负载存储在 B 树叶子节点页上。令 M 为 ((U-12)*32/255)-23,并令 K 为 M+((P-M)%(U-4))。如果 P 大于 X,则存储在表 B 树叶子节点页上的字节数为 K(如果 K 小于或等于 X)或 M(否则)。存储在叶子节点页上的字节数永远不少于 M。
表 B 树内部节点单元格
表 B 树的内部页没有有效负载,因此永远不会有任何有效负载溢出。
索引 B 树叶子节点或内部节点单元格
令 X 为 ((U-12)*64/255)-23。如果有效负载大小 P 小于或等于 X,则整个有效负载存储在 B 树页上。令 M 为 ((U-12)*32/255)-23,并令 K 为 M+((P-M)%(U-4))。如果 P 大于 X,则存储在索引 B 树页上的字节数为 K(如果 K 小于或等于 X)或 M(否则)。存储在索引页上的字节数永远不少于 M。
以下是相同计算的另一种描述
溢出阈值旨在为索引 B 树提供至少 4 的扇出,并确保足够多的有效负载位于 B 树页上,以便通常无需查询溢出页即可访问记录头部。事后看来,SQLite B 树逻辑的设计者意识到这些阈值本可以设计得更简单。但是,在不导致文件格式不兼容的情况下无法更改这些计算。并且当前的计算效果很好,即使它们有点复杂。
当 B 树单元格的有效负载太大而无法容纳在 B 树页中时,多余的部分将溢出到溢出页。溢出页形成一个链接列表。每个溢出页的前四个字节是一个大端整数,表示链中下一个页的页码,或者对于链中的最后一个页,该值为零。第五个字节到最后一个可用字节用于保存溢出内容。
指针映射或 ptrmap 页是插入到数据库中的额外页,以使 自动真空 和 增量真空 模式的工作效率更高。数据库中的其他页类型通常具有从父节点到子节点的指针。例如,一个内部 B 树页包含指向其子 B 树页的指针,而溢出链则具有从链中较早链接到较晚链接的指针。Ptrmap 页包含反向链接信息,从子节点到父节点。
在任何数据库文件中,如果数据库头偏移量 52 处的最大根 B 树页值不为零,则必须存在 ptrmap 页。如果最大根 B 树页值为零,则数据库不得包含 ptrmap 页。
在包含 ptrmap 页的数据库中,第一个 ptrmap 页为第 2 页。Ptrmap 页由一个 5 字节条目数组组成。令 J 为可容纳在页的可使用空间中的 5 字节条目的数量。(换句话说,J=U/5。)第一个 ptrmap 页将包含页 3 到 J+2(含)的反向指针信息。第二个指针映射页将在第 J+3 页上,该 ptrmap 页将提供页 J+4 到 2*J+3(含)的反向指针信息。依此类推,直到整个数据库文件。
在使用 ptrmap 页的数据库中,上一段中计算确定的所有位置的页都必须是 ptrmap 页,并且任何其他页都不能是 ptrmap 页。但例外的是,如果字节锁页碰巧与 ptrmap 页位于同一页码,则在这种情况下,ptrmap 将移动到下一页。
Ptrmap 页上的每个 5 字节条目都提供有关紧跟在指针映射后面的页之一的反向链接信息。如果页 B 是 ptrmap 页,则有关页 B+1 的反向链接信息由指针映射上的第一个条目提供。有关页 B+2 的信息由第二个条目提供。依此类推。
每个 5 字节 ptrmap 条目由一个字节的“页类型”信息和一个 4 字节大端页码组成。识别五种页类型
在任何包含 ptrmap 页的数据库文件中,所有 B 树根页都必须位于任何非根 B 树页、单元格有效负载溢出页或空闲列表页之前。此限制确保在自动真空或增量真空期间永远不会移动根页。自动真空逻辑不知道如何更新 sqlite_schema 表的 root_page 字段,因此有必要防止在自动真空期间移动根页,以维护 sqlite_schema 表的完整性。CREATE TABLE、CREATE INDEX、DROP TABLE 和 DROP INDEX 操作会将根页移动到数据库文件开头。
前文描述了 SQLite 文件格式的底层方面。B 树机制提供了一种强大而高效的方法来访问大型数据集。本节将介绍如何使用底层 B 树层来实现更高级别的 SQL 功能。
表 B 树叶子节点页的数据和索引 B 树页的键在上面被描述为任意字节序列。前面的讨论提到了一个键小于另一个键,但没有定义“小于”的含义。本节将解决这些遗漏。
有效负载(无论是表 B 树数据还是索引 B 树键)始终采用“记录格式”。记录格式定义了一个与表或索引中的列相对应的值序列。记录格式指定列的数量、每列的数据类型以及每列的内容。
记录格式广泛使用了上面定义的 64 位有符号整数的 可变长度整数 或 varint 表示形式。
记录包含头部和主体,按此顺序排列。头部以单个 varint 开始,该 varint 确定头部中的总字节数。varint 值是头部的大小(以字节为单位),包括大小 varint 本身。在大小 varint 之后是一个或多个其他 varint,每个列一个。这些额外的 varint 称为“序列类型”编号,并根据以下图表确定每列的数据类型
序列类型 | 内容大小 | 含义 |
---|---|---|
0 | 0 | 值为 NULL。 |
1 | 1 | 值为 8 位二进制补码整数。 |
2 | 2 | 值为大端 16 位二进制补码整数。 |
3 | 3 | 值为大端 24 位二进制补码整数。 |
4 | 4 | 值为大端 32 位二进制补码整数。 |
5 | 6 | 值为大端 48 位二进制补码整数。 |
6 | 8 | 值为大端 64 位二进制补码整数。 |
7 | 8 | 值为大端 IEEE 754-2008 64 位浮点数。 |
8 | 0 | 值为整数 0。(仅适用于 模式格式 4 及更高版本。) |
9 | 0 | 值为整数 1。(仅适用于 模式格式 4 及更高版本。) |
10,11 | 可变 | 保留供内部使用。这些序列类型代码永远不会出现在格式良好的数据库文件中,但可能会用于 SQLite 有时为了自身使用而生成的临时和临时数据库文件中。这些代码的含义可能会在 SQLite 的不同版本之间发生变化。 |
N≥12 且为偶数 | (N-12)/2 | 值为长度为 (N-12)/2 字节的 BLOB。 |
N≥13 且为奇数 | (N-13)/2 | 值为 文本编码 中的字符串,长度为 (N-13)/2 字节。不存储空终止符。 |
头部大小 varint 和序列类型 varint 通常由单个字节组成。大型字符串和 BLOB 的序列类型 varint 可能会扩展到两个或三个字节的 varint,但这属于例外情况,而不是规则。Varint 格式在编码记录头部方面非常高效。
记录中每列的值紧跟在头部之后。对于序列类型 0、8、9、12 和 13,值长度为零字节。如果所有列都属于这些类型,则记录的主体部分为空。
记录可能具有的值少于相应表中的列数。例如,在 ALTER TABLE ... ADD COLUMN SQL 语句增加了表模式中的列数但未修改表中预先存在的行之后,就会发生这种情况。记录末尾缺少的值将使用表模式中定义的相应列的 默认值 填充。
索引 B 树中键的顺序由键所代表的记录的排序顺序决定。记录比较按列进行。按从左到右的顺序检查记录的列。第一对不相等的列决定了两个记录的相对顺序。各个列的排序顺序如下
为了计算文本字段的顺序,每个列都需要一个 排序函数。SQLite 定义了三个内置的排序函数
BINARY 内置的 BINARY 排序使用标准 C 库中的 memcmp() 函数逐字节比较字符串。 NOCASE NOCASE 排序类似于 BINARY,只是在运行比较之前,大写 ASCII 字符('A' 到 'Z')会折叠成其小写等价物。仅折叠 ASCII 字符。NOCASE 没有实现通用目的的 Unicode 不区分大小写的比较。 RTRIM RTRIM 与 BINARY 类似,但字符串末尾的额外空格不会改变结果。换句话说,只要字符串仅在末尾空格数上存在差异,它们就会被视为相等。
可以使用 sqlite3_create_collation() 接口向 SQLite 添加其他特定于应用程序的排序规则函数。
所有字符串的默认排序规则函数为 BINARY。可以在 CREATE TABLE 语句中使用 COLLATE 子句在表列上指定备选排序规则函数,该子句位于 列定义 中。当对列建立索引时,默认情况下,索引中该列会使用在 CREATE TABLE 语句中指定的相同排序规则函数,不过可以通过 CREATE INDEX 语句中的 COLLATE 子句覆盖此行为。
数据库模式中的每个普通 SQL 表在磁盘上都由一个表 B 树表示。表 B 树中的每个条目对应于 SQL 表中的一行。SQL 表的 rowid 是表 B 树中每个条目的 64 位有符号整数键。
每个 SQL 表行的内容存储在数据库文件中,方法是首先将各个列中的值组合成记录格式的字节数组,然后将该字节数组作为有效负载存储在表 B 树中的条目中。记录中值的顺序与 SQL 表定义中列的顺序相同。当 SQL 表包含 INTEGER PRIMARY KEY 列(它是 rowid 的别名)时,该列在记录中显示为 NULL 值。SQLite 将始终使用表 B 树键而不是 NULL 值来引用 INTEGER PRIMARY KEY 列。
如果列的 亲和性 为 REAL,并且该列包含可以转换为整数而不会丢失信息的值(如果该值不包含小数部分并且不大于整数所能表示的范围),则该列可能会在记录中以整数形式存储。SQLite 会在从记录中提取值时将其转换回浮点数。
如果使用其 CREATE TABLE 语句末尾的 "WITHOUT ROWID" 子句创建 SQL 表,则该表为 WITHOUT ROWID 表,并使用不同的磁盘表示形式。WITHOUT ROWID 表使用索引 B 树而不是表 B 树进行存储。WITHOUT ROWID B 树中每个条目的键由主键列组成的记录,后跟表的所有剩余列组成。主键列按照其在 PRIMARY KEY 子句中声明的顺序出现,其余列按照其在 CREATE TABLE 语句中出现的顺序出现。
因此,WITHOUT ROWID 表的内容编码与普通 rowid 表的内容编码相同,只是列的顺序已重新排列,以便主键列首先出现,并且内容用作索引 B 树中的键,而不是用作表 B 树中的数据。具有 REAL 亲和性的列的特殊编码规则适用于 WITHOUT ROWID 表,就像它们适用于 rowid 表一样。
如果 WITHOUT ROWID 表的主键多次使用具有相同排序顺序的相同列,则主键定义中该列的第二次及后续出现将被忽略。例如,以下 CREATE TABLE 语句都指定了相同的表,该表在磁盘上的表示形式将完全相同
CREATE TABLE t1(a,b,c,d,PRIMARY KEY(a,c)) WITHOUT ROWID; CREATE TABLE t1(a,b,c,d,PRIMARY KEY(a,c,a,c)) WITHOUT ROWID; CREATE TABLE t1(a,b,c,d,PRIMARY KEY(a,A,a,C)) WITHOUT ROWID; CREATE TABLE t1(a,b,c,d,PRIMARY KEY(a,a,a,a,c)) WITHOUT ROWID;
当然,上面的第一个示例是表的首选定义。所有示例都创建了一个 WITHOUT ROWID 表,该表有两个主键列“a”和“c”(按此顺序),后跟两个数据列“b”和“d”(也按此顺序)。
每个 SQL 索引(无论是通过 CREATE INDEX 语句显式声明还是由 UNIQUE 或 PRIMARY KEY 约束隐式声明),都对应于数据库文件中的一个索引 B 树。索引 B 树中的每个条目对应于关联的 SQL 表中的一行。索引 B 树的键由正在索引的列组成的记录,后跟相应表行的键组成。对于普通表,行键为 rowid,对于 WITHOUT ROWID 表,行键为主键。因为表中的每一行都有一个唯一的行键,所以索引中的所有键都是唯一的。
在普通索引中,表中的行与与该表关联的每个索引中的条目之间存在一对一映射。但是,在 部分索引 中,索引 B 树仅包含与 CREATE INDEX 语句上的 WHERE 子句表达式为真的表行对应的条目。索引和表 B 树中的对应行共享相同的 rowid 或主键值,并且对于所有索引列都包含相同的值。
在 WITHOUT ROWID 表上的索引中,如果主键的列也是索引中的列并且具有匹配的排序顺序,则索引记录末尾的表键后缀中不会重复索引列。例如,考虑以下 SQL
CREATE TABLE ex25(a,b,c,d,e,PRIMARY KEY(d,c,a)) WITHOUT rowid; CREATE INDEX ex25ce ON ex25(c,e); CREATE INDEX ex25acde ON ex25(a,c,d,e); CREATE INDEX ex25ae ON ex25(a COLLATE nocase,e);
ex25ce 索引中的每一行都是包含以下列的记录:c、e、d、a。前两列是正在索引的列,c 和 e。其余列是相应表行的主键。通常,主键将是列 d、c 和 a,但由于列 c 已经在索引中较早出现,因此它已从键后缀中省略。
在索引列涵盖主键的所有列的极端情况下,索引将仅包含索引列。上面的 ex25acde 示例演示了这一点。ex25acde 索引中的每个条目仅包含列 a、c、d 和 e(按此顺序)。
ex25ae 中的每一行包含五列:a、e、d、c、a。“a”列重复出现,因为“a”的第一次出现具有“nocase”的排序规则函数,而第二次出现具有“binary”的排序顺序。如果“a”列没有重复,并且表包含两个或多个“e”值相同且“a”仅在大小写上不同的条目,则所有这些表条目将对应于索引中的单个条目,这将破坏表和索引之间的一对一对应关系。
仅在 WITHOUT ROWID 表中才会抑制索引条目键后缀中的冗余列。在普通的 rowid 表中,索引条目始终以 rowid 结尾,即使 INTEGER PRIMARY KEY 列是正在索引的列之一。
数据库文件第 1 页是表 B 树的根页面,该表 B 树保存一个名为“sqlite_schema”的特殊表。此 B 树称为“模式表”,因为它存储完整的数据库模式。“sqlite_schema”表的结构就像使用以下 SQL 创建一样
CREATE TABLE sqlite_schema( type text, name text, tbl_name text, rootpage integer, sql text );
“sqlite_schema”表为数据库模式中的每个表、索引、视图和触发器(统称为“对象”)包含一行,但“sqlite_schema”表本身没有条目。“sqlite_schema”表除了应用程序和程序员定义的对象之外,还包含 内部模式对象 的条目。
“sqlite_schema.type”列将是以下文本字符串之一:“table”、“index”、“view”或“trigger”,具体取决于定义的对象类型。“table”字符串用于普通表和 虚拟表。
“sqlite_schema.name”列将保存对象的名称。表上的 UNIQUE 和 PRIMARY KEY 约束会导致 SQLite 创建名称格式为“sqlite_autoindex_TABLE_N”的 内部索引,其中 TABLE 被包含约束的表的名称替换,N 是从 1 开始的整数,并且在表定义中看到的每个约束都会增加 1。在 WITHOUT ROWID 表中,“sqlite_schema”中没有 PRIMARY KEY 的条目,但“sqlite_autoindex_TABLE_N”名称被保留用于 PRIMARY KEY,就像“sqlite_schema”条目存在一样。这将影响后续 UNIQUE 约束的编号。“sqlite_autoindex_TABLE_N”名称永远不会分配给 INTEGER PRIMARY KEY,无论是在 rowid 表中还是在 WITHOUT ROWID 表中。
“sqlite_schema.tbl_name”列保存对象关联的表或视图的名称。对于表或视图,“tbl_name”列是“name”列的副本。对于索引,“tbl_name”是被索引的表的名称。对于触发器,“tbl_name”列存储导致触发器触发的表或视图的名称。
“sqlite_schema.rootpage”列存储表和索引的根 B 树页面的页码。对于定义视图、触发器和虚拟表的行,“rootpage”列为 0 或 NULL。
“sqlite_schema.sql”列存储描述对象的 SQL 文本。此 SQL 文本是 CREATE TABLE、CREATE VIRTUAL TABLE、CREATE INDEX、CREATE VIEW 或 CREATE TRIGGER 语句,如果在数据库文件是 数据库连接 的主数据库时对此数据库文件进行评估,则会重新创建该对象。该文本通常是用于创建对象的原始语句的副本,但应用了规范化,以便文本符合以下规则
“sqlite_schema.sql”列中的文本是创建对象的原始 CREATE 语句文本的副本,但经过上述描述的规范化以及后续 ALTER TABLE 语句的修改。“sqlite_schema.sql”对于 UNIQUE 或 PRIMARY KEY 约束自动创建的 内部索引 为 NULL。
名称“sqlite_schema”在文件格式中任何地方都没有出现。该名称只是数据库实现使用的约定。由于历史和操作方面的考虑,“sqlite_schema”表有时也可以通过以下别名之一来调用
因为模式表的名称在文件格式中任何地方都没有出现,所以如果应用程序选择通过这些替代名称之一来引用模式表,则数据库文件的意思不会改变。
除了应用程序和/或开发人员使用 CREATE 语句 SQL 创建的表、索引、视图和触发器之外,sqlite_schema 表还可以包含零个或多个内部模式对象的条目,这些条目由 SQLite 为其自身内部使用而创建。内部模式对象的名称始终以“sqlite_”开头,任何名称以“sqlite_”开头的表、索引、视图或触发器都是内部模式对象。SQLite 禁止应用程序创建名称以“sqlite_”开头的对象。
SQLite 使用的内部模式对象可能包括以下内容
名称格式为“sqlite_autoindex_TABLE_N”的索引,用于实现普通表上的UNIQUE和PRIMARY KEY约束。
名称为“sqlite_sequence”的表,用于跟踪使用AUTOINCREMENT的表的最大历史INTEGER PRIMARY KEY。
名称格式为“sqlite_statN”的表,其中 N 是一个整数。此类表存储由ANALYZE命令收集的数据库统计信息,查询规划器使用这些信息来帮助确定每个查询使用的最佳算法。
将来版本中可能会将新的内部模式对象名称(始终以“sqlite_”开头)添加到 SQLite 文件格式。
sqlite_sequence 表是一个内部表,用于帮助实现AUTOINCREMENT。每当创建任何具有 AUTOINCREMENT 整数主键的普通表时,都会自动创建 sqlite_sequence 表。创建后,sqlite_sequence 表将永远存在于 sqlite_schema 表中;它无法被删除。sqlite_sequence 表的模式如下
CREATE TABLE sqlite_sequence(name,seq);
对于每个使用 AUTOINCREMENT 的普通表,sqlite_sequence 表中都有一行。表的名称(如 sqlite_schema.name 中所示)位于 sqlite_sequence.name 字段中,曾经插入该表中的最大INTEGER PRIMARY KEY位于 sqlite_sequence.seq 字段中。AUTOINCREMENT 表的新自动生成的整数主键保证大于该表的 sqlite_sequence.seq 字段。如果 AUTOINCREMENT 表的 sqlite_sequence.seq 字段已经达到最大整数值 (9223372036854775807),则尝试向该表添加具有自动生成的整数主键的新行将失败,并出现SQLITE_FULL错误。如果需要,在向 AUTOINCREMENT 表插入新条目时,sqlite_sequence.seq 字段会自动更新。当表被删除时,AUTOINCREMENT 表的 sqlite_sequence 行会自动删除。如果在更新 AUTOINCREMENT 表时 AUTOINCREMENT 表的 sqlite_sequence 行不存在,则会创建一个新的 sqlite_sequence 行。如果 AUTOINCREMENT 表的 sqlite_sequence.seq 值被手动设置为非整数,并且随后尝试插入或更新 AUTOINCREMENT 表,则行为未定义。
应用程序代码允许修改 sqlite_sequence 表,添加新行、删除行或修改现有行。但是,如果 sqlite_sequence 表不存在,则应用程序代码无法创建它。应用程序代码可以删除 sqlite_sequence 表中的所有条目,但应用程序代码无法删除 sqlite_sequence 表。
sqlite_stat1 是由ANALYZE命令创建的内部表,用于保存有关表和索引的补充信息,查询规划器可以使用这些信息来帮助找到执行查询的更好方法。应用程序可以更新、删除、插入或删除 sqlite_stat1 表,但不能创建或修改 sqlite_stat1 表。sqlite_stat1 表的模式如下
CREATE TABLE sqlite_stat1(tbl,idx,stat);
通常每个索引有一行,索引由 sqlite_stat1.idx 列中的名称标识。sqlite_stat1.tbl 列是索引所属表的名称。在每一行中,sqlite_stat.stat 列将是一个字符串,该字符串包含一系列整数,后跟零个或多个参数。此列表中的第一个整数是索引中大约的行数。(索引中的行数与表中的行数相同,部分索引除外。)第二个整数是索引中第一列具有相同值的近似行数。第三个整数是索引中前两列具有相同值的行的数量。第 N 个整数(对于 N>1)是索引中前 N-1 列具有相同值的行的估计平均数量。对于 K 列索引,stat 列中将有 K+1 个整数。如果索引是唯一的,则最后一个整数将为 1。
stat 列中的整数列表之后可以可选地跟参数,每个参数都是一系列非空格字符。所有参数前面都带有一个空格。无法识别的参数将被静默忽略。
如果存在“unordered”参数,则查询规划器假设索引是无序的,并且不会将索引用于范围查询或排序。
“sz=NNN”参数(其中 NNN 表示一个或多个数字的序列)表示表或索引所有记录的平均行大小为每行 NNN 字节。SQLite 查询规划器可能会使用“sz=NNN”标记提供的估计行大小信息来帮助它选择需要较少磁盘 I/O 的较小表和索引。
索引的 sqlite_stat1.stat 字段中存在“noskipscan”标记会阻止该索引与跳过扫描优化一起使用。
将来对 SQLite 的增强可能会在 stat 列的末尾添加新的文本标记。为了兼容性,stat 列末尾的无法识别的标记将被静默忽略。
如果 sqlite_stat1.idx 列为 NULL,则 sqlite_stat1.stat 列包含一个整数,该整数是 sqlite_stat1.tbl 标识的表中的近似行数。如果 sqlite_stat1.idx 列与 sqlite_stat1.tbl 列相同,则该表是WITHOUT ROWID表,并且 sqlite_stat1.stat 字段包含有关实现 WITHOUT ROWID 表的索引 btree 的信息。
sqlite_stat2 仅在 SQLite 使用 SQLITE_ENABLE_STAT2 编译并且 SQLite 版本号介于 3.6.18 (2009-09-11) 和 3.7.8 (2011-09-19) 之间时创建和使用。3.6.18 之前的任何版本的 SQLite 以及 3.7.8 之后的任何版本都不会读取或写入 sqlite_stat2 表。sqlite_stat2 表包含有关索引内键分布的其他信息。sqlite_stat2 表的模式如下
CREATE TABLE sqlite_stat2(tbl,idx,sampleno,sample);
sqlite_stat2 表中每一行的 sqlite_stat2.idx 列和 sqlite_stat2.tbl 列标识该行描述的索引。sqlite_stat2 表中每个索引通常有 10 行。
索引的 sqlite_stat2 条目,其中 sqlite_stat2.sampleno 介于 0 和 9(含)之间,是从索引中均匀间隔的点获取的索引最左侧键值的样本。令 C 为索引中的行数。然后,采样行由以下公式给出:
rownumber = (i*C*2 + C)/20
前一个表达式中的变量 i 在 0 到 9 之间变化。从概念上讲,索引空间被分成 10 个均匀的桶,样本是每个桶的中间行。
sqlite_stat2 的格式在此记录以供旧版参考。SQLite 的最新版本不再支持 sqlite_stat2 和 sqlite_stat2 表,如果存在,则会被简单地忽略。
sqlite_stat3 仅在 SQLite 使用SQLITE_ENABLE_STAT3或SQLITE_ENABLE_STAT4编译并且 SQLite 版本号为 3.7.9 (2011-11-01) 或更高版本时使用。3.7.9 之前的任何版本的 SQLite 都不会读取或写入 sqlite_stat3 表。如果使用SQLITE_ENABLE_STAT4编译时选项并且 SQLite 版本号为 3.8.1 (2013-10-17) 或更高版本,则 sqlite_stat3 可能会被读取但不会被写入。sqlite_stat3 表包含有关索引内键分布的其他信息,查询规划器可以使用这些信息来设计更好、更快的查询算法。sqlite_stat3 表的模式如下
CREATE TABLE sqlite_stat3(tbl,idx,nEq,nLt,nDLt,sample);
每个索引的 sqlite_stat3 表中通常有多个条目。sqlite_stat3.sample 列保存由 sqlite_stat3.idx 和 sqlite_stat3.tbl 标识的索引的最左侧字段的值。sqlite_stat3.nEq 列保存索引中其最左侧列与样本完全匹配的条目的近似数量。sqlite_stat3.nLt 保存索引中其最左侧列小于样本的条目的近似数量。sqlite_stat3.nDLt 列保存索引中小于样本的不同最左侧条目的近似数量。
每个索引可以有任意数量的 sqlite_stat3 条目。ANALYZE命令通常会生成包含 10 到 40 个样本的 sqlite_stat3 表,这些样本分布在键空间中并且具有较大的 nEq 值。
在格式良好的 sqlite_stat3 表中,任何单个索引的样本必须以它们在索引中出现的相同顺序出现。换句话说,如果最左侧列为 S1 的条目在索引 b 树中早于最左侧列为 S2 的条目,则在 sqlite_stat3 表中,样本 S1 的行 ID 必须小于样本 S2 的行 ID。
sqlite_stat4 仅在 SQLite 使用SQLITE_ENABLE_STAT4编译并且 SQLite 版本号为 3.8.1 (2013-10-17) 或更高版本时创建和使用。3.8.1 之前的任何版本的 SQLite 都不会读取或写入 sqlite_stat4 表。sqlite_stat4 表包含有关索引内键分布或WITHOUT ROWID表的主键中键分布的其他信息。查询规划器有时可以使用 sqlite_stat4 表中的其他信息来设计更好、更快的查询算法。sqlite_stat4 表的模式如下
CREATE TABLE sqlite_stat4(tbl,idx,nEq,nLt,nDLt,sample);
每个具有可用统计信息的索引的 sqlite_stat4 表中通常有 10 到 40 个条目,但是这些限制不是硬性限制。sqlite_stat4 表中各列的含义如下
tbl | sqlite_stat4.tbl 列保存拥有该行描述的索引的表的名称 |
idx | sqlite_stat4.idx 列保存该行描述的索引的名称,或者在WITHOUT ROWID表的 sqlite_stat4 条目的情况下,保存表本身的名称。 |
sample | sqlite_stat4.sample 列保存以记录格式编码的 BLOB,该格式对索引列进行编码,然后是行 ID 表的行 ID 或 WITHOUT ROWID 表的主键列。WITHOUT ROWID 表本身的 sqlite_stat4.sample BLOB 仅包含主键的列。令 sqlite_stat4.sample blob 编码的列数为 N。对于普通行 ID 表上的索引,N 将比索引列数多 1。对于 WITHOUT ROWID 表上的索引,N 将等于索引列数加上主键中的列数。对于 WITHOUT ROWID 表,N 将等于主键中的列数。 |
nEq | sqlite_stat4.nEq 列保存 N 个整数的列表,其中第 K 个整数是索引中其最左侧 K 列与样本的最左侧 K 列完全匹配的条目的近似数量。 |
nLt | sqlite_stat4.nLt 列保存 N 个整数的列表,其中第 K 个整数是索引中其 K 个最左侧列组合小于样本的 K 个最左侧列的条目的近似数量。 |
nDLt | sqlite_stat4.nDLt 列包含一个 N 个整数的列表,其中第 K 个整数是索引中大约有多少个条目在最初的 K 列中是不同的,并且最左边的 K 列都小于样本的最左边的 K 列。 |
sqlite_stat4 是 sqlite_stat3 表的泛化。sqlite_stat3 表提供了关于索引最左列的信息,而 sqlite_stat4 表提供了关于索引所有列的信息。
每个索引可以有任意数量的 sqlite_stat4 条目。 ANALYZE 命令通常会生成包含 10 到 40 个样本的 sqlite_stat4 表,这些样本分布在键空间中,并且具有较大的 nEq 值。
在一个格式良好的 sqlite_stat4 表中,任何单个索引的样本必须按照它们在索引中出现的顺序出现。换句话说,如果条目 S1 在索引 B 树中比条目 S2 更早,那么在 sqlite_stat4 表中,样本 S1 的 rowid 必须小于样本 S2。
回滚日志是与每个 SQLite 数据库文件关联的文件,其中包含用于在事务过程中将数据库文件恢复到其初始状态的信息。回滚日志文件始终位于与数据库文件相同的目录中,并且与数据库文件具有相同的名称,但附加了字符串"-journal"。每个数据库只能关联一个回滚日志,因此一次只能对单个数据库打开一个写事务。
在修改数据库的任何包含信息的页面之前,该页面的原始未修改内容将写入回滚日志。如果事务被中断且需要回滚,则可以使用回滚日志将数据库恢复到其原始状态。空闲列表叶页面不包含在回滚时需要恢复的信息,因此在修改之前不会将其写入日志,以减少磁盘 I/O。
如果事务由于应用程序崩溃、单个崩溃或操作系统崩溃、硬件电源故障或崩溃而中止,则主数据库文件可能会处于不一致的状态。下次 SQLite 尝试打开数据库文件时,将检测到回滚日志文件的存在,并且该日志将自动回放以将数据库恢复到不完整事务开始时的状态。
只有当回滚日志存在且包含有效标头时,才认为它是有效的。因此,事务可以通过三种方式之一提交
这三种提交事务的方式分别对应于 journal_mode pragma 的 DELETE、TRUNCATE 和 PERSIST 设置。
有效的回滚日志以以下格式的标头开头
偏移量 | 大小 | 描述 |
---|---|---|
0 | 8 | 标头字符串:0xd9、0xd5、0x05、0xf9、0x20、0xa1、0x63、0xd7 |
8 | 4 | "页面计数" - 日志下一段中的页面数,或 -1 表示文件末尾的所有内容 |
12 | 4 | 用于校验和的随机随机数 |
16 | 4 | 数据库的初始页面大小。 |
20 | 4 | 写入此日志的进程假设的磁盘扇区大小。 |
24 | 4 | 此日志中页面的大小。 |
回滚日志标头用零填充到单个扇区的大小(由偏移量 20 处的扇区大小整数定义)。标头位于一个扇区中,以便如果在写入扇区时发生电源故障,则标头后面的信息将(希望)不会损坏。
在标头和零填充之后是零个或多个页面记录。每个页面记录存储数据库文件中某个页面的内容副本,在该页面更改之前。同一页面在一个回滚日志中最多只能出现一次。要回滚不完整的事务,进程只需从头到尾读取回滚日志,并将日志中找到的页面写入数据库文件的适当位置即可。
假设数据库页面大小(日志标头中偏移量 24 处的整数的值)为 N。则页面记录的格式如下
偏移量 | 大小 | 描述 |
---|---|---|
0 | 4 | 数据库文件中的页面号 |
4 | N | 事务开始之前页面的原始内容 |
N+4 | 4 | 校验和 |
校验和是一个无符号的 32 位整数,计算方法如下
校验和值用于防止电源故障后日志页面记录的写入不完整。每次启动事务时都会使用不同的随机随机数,以最大程度地降低未写入的扇区可能偶然包含先前日志中相同页面的数据的风险。通过更改每个事务的随机数,磁盘上的陈旧数据仍然会生成错误的校验和,并且很有可能被检测到。出于性能原因,校验和仅使用数据记录中 32 位字的稀疏样本 - 在 SQLite 3.0.0 的计划阶段进行的设计研究表明,对整个页面进行校验和会显着降低性能。
假设日志标头中偏移量 8 处的页面计数值为 M。如果 M 大于零,则在 M 个页面记录之后,日志文件可能会用零填充到下一个扇区大小的倍数,并且可以插入另一个日志标头。同一日志中的所有日志标头必须包含相同的数据库页面大小和扇区大小。
如果初始日志标头中的 M 为 -1,则跟随的页面记录数通过计算日志文件剩余可用空间中可以容纳多少个页面记录来计算。
从 3.7.0 版(2010-07-21)开始,SQLite 支持一种名为 "预写日志" 或 "WAL" 的新事务控制机制。当数据库处于 WAL 模式时,对该数据库的所有连接都必须使用 WAL。特定的数据库将使用回滚日志或 WAL,但不能同时使用两者。WAL 始终位于与数据库文件相同的目录中,并且与数据库文件具有相同的名称,但附加了字符串"-wal"。
WAL 文件 由一个标头和零个或多个“帧”组成。每个帧记录数据库文件中单个页面的修改内容。对数据库的所有更改都通过将帧写入 WAL 来记录。当写入包含提交标记的帧时,事务提交。单个 WAL 可以并且通常会记录多个事务。定期地,WAL 的内容将通过称为“检查点”的操作传输回数据库文件。
单个 WAL 文件可以重复使用多次。换句话说,WAL 可以填满帧,然后进行检查点,然后新的帧可以覆盖旧的帧。WAL 总是从开头向结尾增长。附加到每个帧的校验和和计数器用于确定 WAL 中哪些帧有效,哪些帧是先前检查点的残留物。
WAL 标头大小为 32 字节,由以下八个大端 32 位无符号整数值组成
偏移量 | 大小 | 描述 |
---|---|---|
0 | 4 | 幻数。0x377f0682 或 0x377f0683 |
4 | 4 | 文件格式版本。当前为 3007000。 |
8 | 4 | 数据库页面大小。例如:1024 |
12 | 4 | 检查点序列号 |
16 | 4 | Salt-1:每次检查点都会递增的随机整数 |
20 | 4 | Salt-2:每个检查点不同的随机数 |
24 | 4 | Checksum-1:标头前 24 字节的校验和的第一部分 |
28 | 4 | Checksum-2:标头前 24 字节的校验和的第二部分 |
紧随 wal 标头之后是零个或多个帧。每个帧由一个 24 字节的帧标头和 页面大小 字节的页面数据组成。帧标头是六个大端 32 位无符号整数值,如下所示
偏移量 | 大小 | 描述 |
---|---|---|
0 | 4 | 页面号 |
4 | 4 | 对于提交记录,提交后数据库文件的大小(以页面为单位)。对于所有其他记录,为零。 |
8 | 4 | 从 WAL 标头复制的 Salt-1 |
12 | 4 | 从 WAL 标头复制的 Salt-2 |
16 | 4 | Checksum-1:到包括此页面为止的累积校验和 |
20 | 4 | Checksum-2:累积校验和的后半部分。 |
当且仅当以下条件为真时,帧才被视为有效
帧标头中的 salt-1 和 salt-2 值与 wal 标头中的 salt 值匹配
帧标头最后 8 字节中的校验和值与 WAL 标头前 24 字节以及所有帧的前 8 字节和内容(直到且包括当前帧)连续计算的校验和完全匹配。
校验和通过将输入解释为偶数个无符号 32 位整数来计算:x(0) 到 x(N)。如果 WAL 标头前 4 字节中的幻数为 0x377f0683,则 32 位整数为大端,如果幻数为 0x377f0682,则整数为小端。无论使用哪种字节顺序计算校验和,校验和值始终以大端格式存储在帧标头中。
校验和算法仅适用于长度为 8 字节倍数的内容。换句话说,如果输入是 x(0) 到 x(N),则 N 必须为奇数。校验和算法如下
s0 = s1 = 0 for i from 0 to n-1 step 2: s0 += x(i) + s1; s1 += x(i+1) + s0; endfor # result in s0 and s1
输出 s0 和 s1 都是使用斐波那契权重(反向顺序)的加权校验和。(最大的斐波那契权重出现在正在求和的序列的第一个元素上。)s1 值跨越序列的所有 32 位整数项,而 s0 省略最后一项。
在 检查点 上,WAL 首先使用 VFS 的 xSync 方法刷新到持久存储。然后,WAL 的有效内容将传输到数据库文件。最后,数据库使用另一个 xSync 方法调用刷新到持久存储。xSync 操作充当写屏障 - 在 xSync 之前启动的所有写入必须在 xSync 之后启动的任何写入开始之前完成。
检查点不需要运行到完成。可能是某些读取器仍在使用数据库文件中包含的数据的旧事务。在这种情况下,将 WAL 文件中较新事务的内容传输到数据库将删除读取器仍在使用旧事务的内容。为了避免这种情况,只有当所有读取器都使用 WAL 中的最后一个事务时,检查点才会运行到完成。
在完成一次检查点后,如果没有任何其他连接处于使用 WAL 的事务中,则后续的写事务可以从头开始覆盖 WAL 文件。这被称为“重置 WAL”。在第一个新的写事务开始时,WAL 头部的 salt-1 值会递增,salt-2 值会随机化。这些对 salt 的更改会使 WAL 中已经检查点但尚未覆盖的旧帧失效,并防止它们再次被检查点。
WAL 文件可以选择在重置时被截断,但它不必被截断。如果 WAL 没有被截断,性能通常会稍微好一点,因为文件系统通常会比扩展文件更快地覆盖现有文件。
要从数据库读取一个页面(称其为页面号 P),读取器首先检查 WAL 以查看它是否包含页面 P。如果是,则页面 P 的最后一个有效实例(后面跟着一个提交帧或本身就是一个提交帧)成为读取的值。如果 WAL 不包含页面 P 的任何有效副本,并且这些副本是提交帧或后面跟着提交帧,则从数据库文件中读取页面 P。
要启动一个读取事务,读取器会将 WAL 中值帧的数量记录为“mxFrame”。(更多细节) 读取器在所有后续读取操作中使用此记录的 mxFrame 值。新的事务可以追加到 WAL,但是只要读取器使用其原始 mxFrame 值并忽略随后追加的内容,读取器就会看到数据库在某个时间点的某个特定时刻的一致快照。此技术允许多个并发读取器同时查看数据库内容的不同版本。
前面段落中的读取器算法可以正常工作,但由于页面 P 的帧可以出现在 WAL 中的任何位置,因此读取器必须扫描整个 WAL 以查找页面 P 帧。如果 WAL 很大(通常为几兆字节),则扫描可能会很慢,读取性能会下降。为了克服这个问题,维护了一个称为 wal-index 的单独数据结构来加快对特定页面帧的搜索。
从概念上讲,wal-index 是共享内存,尽管当前的 VFS 实现使用内存映射文件来实现操作系统可移植性。内存映射文件位于与数据库相同的目录中,并且与数据库具有相同的名称,并在后面附加了“-shm"后缀。因为 wal-index 是共享内存,所以当客户端位于不同的机器上时,SQLite 不支持在网络文件系统上使用journal_mode=WAL,因为数据库的所有客户端都必须能够共享相同的内存。
wal-index 的目的是快速回答这个问题
给定一个页面号 P 和一个最大 WAL 帧索引 M,返回不超过 M 的页面 P 的最大 WAL 帧索引,或者如果不存在不超过 M 的页面 P 的帧,则返回 NULL。
上一段中的M值是在第 4.4 节中定义的“mxFrame”值,该值在事务开始时读取,并定义读取器将使用的 WAL 的最大帧。
wal-index 是临时的。发生崩溃后,将从原始 WAL 文件重建 wal-index。VFS 要求在最后一个连接关闭时截断或清零 wal-index 的头。由于 wal-index 是临时的,因此它可以使用特定于体系结构的格式;它不必是跨平台的。因此,与将所有值存储为大端序的数据库和 WAL 文件格式不同,wal-index 以主机计算机的本地字节序存储多字节值。
本文档关注的是数据库文件的持久状态,并且由于 wal-index 是一个临时结构,因此此处不会提供有关 wal-index 格式的更多信息。有关 wal-index 格式的更多详细信息包含在单独的WAL-索引文件格式文档中。
此页面上次修改于 2024-07-24 10:03:34 UTC