返回:SQLite—系列文章目录
上一篇:SQLite 4.9的虚拟表机制(十四)
下一篇:SQLite超详细的编译时选项(十六)
本文档描述和定义磁盘上的数据库文件 自 SQLite 以来所有版本使用的格式 版本 3.0.0 (2004-06-18).
1. 数据库文件
SQLite数据库的完整状态通常是 包含在磁盘上的单个文件中,称为“主数据库文件”。
在事务期间,SQLite存储其他信息 在称为“回滚日志”的第二个文件中,或者如果 SQLite 处于 WAL 模式,则为预写日志文件。
1.1. 热门期刊
如果应用程序或 主机在事务完成之前崩溃,然后回滚 日志或预写日志包含所需的信息 将主数据库文件还原到一致状态。回滚时 日志或预写日志包含恢复所需的信息 数据库的状态,它们被称为“热日志”或“热WAL文件”。 热日志和WAL文件只是错误恢复过程中的一个因素 场景等并不常见,但它们是 SQLite 状态的一部分 数据库等不能忽视。本文档定义了格式 回滚日志和预写日志文件,但重点是 在主数据库文件上。
1.2. 页面
主数据库文件由一个或多个页面组成。的大小 Page 是介于 512 和 65536 之间的 2 次幂(含)。内所有页面 相同的数据库具有相同的大小。数据库文件的页面大小 由位于 从数据库文件开头开始的 16 个字节。
页码以 1 开头。最大页码为 4294967294(2,32-2)。最小尺寸 SQLite 数据库是一个 512 字节的页面。 最大大小的数据库将是 4294967294 页,每 65536 字节 页面或 281,474,976,579,584 字节(约 281 TB)。通常 SQLite 会 达到底层文件系统或磁盘的最大文件大小限制 硬件早在它达到自己的内部大小限制之前。
在通常的使用中,SQLite数据库的大小往往从几千字节不等 到几千兆字节,尽管已知存在 TB 大小的 SQLite 数据库 在生产中。
在任何时间点,主数据库中的每个页面都有一个 使用以下方法之一:
- 锁定字节页面
- 自由列表页面
- 自由列表主干页面
- 自由列表叶页面
- b 树页面
- 表格b-tree内页
- 表 b 树叶页
- 索引 b 树内页
- 索引 b 树叶页
- 有效负载溢出页面
- 指针映射页
对主数据库文件的所有读取和写入都从一页开始 boundary 和 all writes 的大小为整数页数。读 通常也是整数页的大小,但有一个例外 当数据库首次打开时,前 100 个字节的 数据库文件(数据库文件头)作为子页面大小单位读取。
在修改数据库的任何信息页面之前, 该页面的原始未修改内容将写入 回滚日志。如果事务中断并且需要 回滚,然后可以使用回滚日志来恢复 数据库恢复到其原始状态。Freelist 叶子页没有 需要在回滚时还原的信息,因此它们 在修改之前没有写入期刊,以便 减少磁盘 I/O。
1.3. 数据库头
数据库文件的前 100 个字节构成数据库文件 页眉。数据库文件头分为多个字段,如下所示 下表。数据库文件头中的所有多字节字段都是 首先使用最高有效字节 (big-endian) 存储。
数据库头格式
偏移 | 大小 | 说明 |
---|---|---|
0 | 16 | 标头字符串:“SQLite 格式 3\000” |
16 | 2 | 数据库页面大小(以字节为单位)。必须是 2 之间的 512 次幂 和 32768(含 32768),或值 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 | 数据库文件的大小(以页为单位)。“in-header 数据库大小”。 |
32 | 4 | 第一个自由列表主干页面的页码。 |
36 | 4 | 免费列表页面总数。 |
40 | 4 | 架构 cookie。 |
44 | 4 | 架构格式编号。支持的架构格式为 1、2、3 和 4。 |
48 | 4 | 默认页面缓存大小。 |
52 | 4 | 自动抽真空或 增量真空模式,否则为零。 |
56 | 4 | 数据库文本编码。值为 1 表示 UTF-8。值为 2 表示 UTF-16le。值 3 表示 UTF-16be。 |
60 | 4 | 由user_version编译指示读取和设置的“用户版本”。 |
64 | 4 | 增量真空模式为 True(非零)。否则为 false(零)。 |
68 | 4 | PRAGMA 设置的“应用程序 ID”application_id。 |
72 | 20 | 保留用于扩展。必须为零。 |
92 | 4 | version-valid-for 编号。 |
96 | 4 | SQLITE_VERSION_NUMBER |
1.3.1. 魔术头字符串
每个有效的 SQLite 数据库文件都以下列 16 个字节开头 (十六进制):53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00。此字节序列 对应于 UTF-8 字符串“SQLite format 3”,包括 nul 末尾的终结者角色。
1.3.2. 页面大小
从偏移量 16 开始的双字节值决定了 数据库。对于 SQLite 版本 3.7.0.1 (2010-08-04) 在更早的时候,这个值是 解释为大端整数,并且必须是 2 之间的幂 512 和 32768(含)。从 SQLite 版本 3.7.1 (2010-08-23) 开始,一个页面 支持 65536 字节的大小。值 65536 不适合 双字节整数,因此要指定 65536 字节的页面大小,值 偏移量为 16 0x00 0x01。 此值可以解释为大端 1 并被认为是一个表示 65536 页面大小的幻数。 或者可以将两字节字段视为一个小字节数,然后说 它表示页面大小除以 256。这两个 对 page-size 字段的解释是等效的。
1.3.3. 文件格式版本号
偏移的文件格式写入版本和文件格式读取版本 18 和 19 旨在允许增强文件格式 在 SQLite 的未来版本中。在当前版本的 SQLite 中,两者都 对于回滚日记模式,这些值为 1,对于 WAL 日记模式,这些值为 2。如果 SQLite 的版本编码为当前 文件格式规范遇到一个数据库文件,其中读取 version 为 1 或 2,但写入版本大于 2,则数据库 文件必须被视为只读。如果具有读取版本的数据库文件 如果遇到大于 2,则无法读取或写入该数据库。
1.3.4. 每页保留字节数
SQLite能够在 每个页面的末尾,供扩展使用。这些额外的字节是 例如,SQLite 加密扩展用于存储随机数 和/或与每个页面关联的加密校验和。这 偏移量 20 处的 1 字节整数中的“保留空间”大小是数字 每页末尾的字节空间,以保留扩展名。 此值通常为 0。该值可能是奇数。
数据库页的“可用大小”是 标头中偏移量 16 处的 2 字节整数减去“保留”空间大小 记录在标头偏移量 20 处的 1 字节整数中。可用 页面大小可能是奇数。但是,可用尺寸不是 允许小于 480。换句话说,如果页面大小为 512, 则预留空间大小不能超过 32。
1.3.5. 有效载荷分数
最大和最小嵌入有效载荷分数和叶片 有效负载分数值必须为 64、32 和 32。这些值是 最初打算成为可用于 修改 B 树算法的存储格式。但是,那 不支持功能,并且当前没有要添加的计划 未来的支持。因此,这三个字节固定在 指定的值。
1.3.6. 文件更改计数器
文件更改计数器是一个 4 字节的大端整数,位于 每当数据库文件解锁时递增的偏移量 24 修改后。 当两个或多个进程读取同一个数据库文件时,每个进程 进程可以通过监视来检测来自其他进程的数据库更改 找零钱计数器。 在以下情况下,进程通常希望刷新其数据库页缓存 另一个进程修改了数据库,因为缓存已过时。 文件更改计数器便于执行此操作。
在 WAL 模式下,使用 wal-index 检测对数据库的更改 因此不需要找零计数器。因此,更改计数器可能 在 WAL 模式下,每个事务都不会递增。
1.3.7. Header 数据库大小
偏移量为 28 处的 4 字节大端整数进入标头 以页为单位存储数据库文件的大小。如果此 in-header datasize size 无效(见下一段),则数据库 大小是通过查看来计算的 以数据库文件的实际大小。SQLite 的旧版本 忽略头数据库大小,使用实际文件大小 惟独。较新版本的 SQLite 使用 in-header 数据库 size(如果可用),但回退到实际文件大小,如果 In-Header 数据库大小无效。
仅当以下情况时,才认为标头内数据库大小有效 它不为零,如果 4 字节更改计数器在偏移量 24 处 与偏移量 92 处的 4 字节 version-valid-for 数字完全匹配。 标头内数据库大小始终有效 当仅使用最新版本的 SQLite 修改数据库时, 版本 3.7.0 (2010-07-21) 及更高版本。 如果旧版本的SQLite写入数据库,则不会 知道更新 In-Header 数据库大小,因此 In-Header 数据库大小可能不正确。但是SQLite的旧版本 还将保持偏移量 92 处的 version-valid-for 编号不变 所以它不会与更改计数器匹配。因此,无效的 in-header 可以通过观察何时来检测(并忽略)数据库大小 更改计数器与 version-valid-for 编号不匹配。
1.3.8. 免费页面列表
数据库文件中未使用的页面存储在自由列表上。这 偏移量 32 处的 4 字节大端整数存储 自由列表的第一页,如果自由列表为空,则为零。 偏移量 36 处的 4 字节大端整数存储总计 自由列表上的页数。
1.3.9. 架构 cookie
架构 cookie 是一个偏移量为 40 的 4 字节大端整数 每当数据库架构发生更改时,该值都会递增。一个 准备好的语句是针对特定版本的 数据库架构。当数据库架构发生更改时,语句 必须重新准备。当准备好的语句运行时,它首先检查 架构 cookie,以确保值与语句时的值相同 已准备,如果架构 cookie 已更改,则语句 自动重新准备并重新运行,否则会因SQLITE_SCHEMA错误而中止。
1.3.10. 模式格式编号
架构格式编号是偏移量为 44 处的 4 字节大端整数。 架构格式编号类似于文件格式读取和写入 偏移量 18 和 19 处的版本号,但架构格式号除外 指高级 SQL 格式,而不是低级 b 树 格式。目前定义了四个架构格式编号:
- 格式 1 被所有版本的 SQLite 理解到版本 3.0.0 (2004-06-18)。
- 格式 2 添加了同一表中行的功能 具有不同数量的列,以支持 ALTER TABLE ...ADD COLUMN 功能。支持 读写格式 2 在 2005-02-20 的 SQLite 版本 3.1.3 中添加了。
- 格式 3 添加了 ALTER TABLE 添加的额外列的功能...ADD COLUMN 设置为非 NULL 默认值 值。此功能是在 2005-03-11 的 SQLite 版本 3.1.4 中添加的。
- 格式 4 导致 SQLite 遵循 DESC 关键字 on 索引声明。(DESC 关键字在索引中被忽略 格式 1、2 和 3。 格式 4 还添加了两个新的布尔记录类型值(序列号类型 8 和 9)。在 SQLite 3.3.0 中添加了对格式 4 的支持 2006-01-10.
默认情况下,SQLite 创建的新数据库文件使用格式 4。 legacy_file_format编译指示可用于导致 SQLite 使用格式 1 创建新的数据库文件。 格式版本号可以通过以下方式默认为 1 而不是 4 在编译时设置 SQLITE_DEFAULT_FILE_FORMAT=1。
1.3.11. 建议的缓存大小
偏移量为 48 处的 4 字节大端有符号整数是建议的 数据库文件的缓存大小(以页为单位)。该值是一个建议 只有,SQLite没有义务履行它。绝对值 的整数用作建议的大小。建议的缓存大小 可以使用default_cache_size Pragma 进行设置。
1.3.12. 增量真空设置
使用偏移量 52 和 64 处的两个 4 字节大端整数 管理auto_vacuum和incremental_vacuum模式。如果 偏移量 52 处的整数为零,则指针映射 (PTRMAP) 页面为 从数据库文件中省略,既不auto_vacuum也不 支持incremental_vacuum。如果偏移量 52 处的整数为 非零,则它是 数据库文件,数据库文件将包含 ptrmap 页面,并且 mode 必须是 auto_vacuum 或 incremental_vacuum。在后者中 情况下,偏移量 64 处的整数对于 incremental_vacuum 和 auto_vacuum为 false。如果偏移量 52 处的整数为零,则 偏移量 64 处的整数也必须为零。
1.3.13. 文本编码
偏移量 56 处的 4 字节大端整数确定编码 用于存储在数据库中的所有文本字符串。 值为 1 表示 UTF-8。 值 2 表示 UTF-16le。 值 3 表示 UTF-16be。 不允许使用其他值。 sqlite3.h 头文件将 C 预处理器宏SQLITE_UTF8定义为 1, SQLITE_UTF16LE 为 2,SQLITE_UTF16BE为 3,用于代替 文本编码的数字代码。
1.3.14. 用户版本号
偏移量为 60 处的 4 字节大端整数是用户版本,其 由user_version Pragma 设置和查询。用户版本是 SQLite未使用。
1.3.15. 应用程序 ID
偏移量 68 处的 4 字节大端整数是一个“应用程序 ID”,它 可以通过 PRAGMA application_id 命令进行设置,以识别 数据库属于特定应用程序或与特定应用程序相关联。 应用程序 ID 用于用作应用程序文件格式的数据库文件。应用程序 ID 可由实用程序使用 比如 file(1) 来确定具体的 文件类型,而不仅仅是报告“SQLite3 数据库”。列表 通过查阅 SQLite 源存储库中的 magic.txt 文件,可以查看分配的应用程序 ID。
1.3.16. 写入库版本号和版本有效号
偏移量 96 处的 4 字节大端整数存储了 SQLite 库的SQLITE_VERSION_NUMBER值,该值最 最近修改了数据库文件。4 字节大端整数 偏移量 92 是版本号时更改计数器的值 被存储。偏移量 92 处的整数表示哪个事务 版本号对 有效,有时称为 “version-valid-for 编号”。
1.3.17. 为扩展保留的标题空间
数据库文件头的所有其他字节都保留给 将来的扩展,并且必须设置为零。
1.4. 锁字节页面
锁字节页是数据库文件的单个页 包含 1073741824 和 1073742335 之间偏移的字节, 包容。小于或等于 1073741824 字节的数据库文件 in size 不包含锁字节页。大于 1073741824只包含一个锁字节页。
锁字节页被留出,供特定于操作系统的 VFS 实现在实现数据库文件锁定基元时使用。 SQLite 不使用锁字节页。SQLite核心 永远不会读取或写入锁字节页, 尽管特定于操作系统的 VFS 实现可以选择在锁字节上读取或写入字节 页面根据 底层系统的需求和倾向。内置到 SQLite 中的 unix 和 win32 VFS 实现不会写入 锁定字节页面,但第三方 VFS 实现 其他操作系统可能。
锁字节页面源于支持 Win95 的需要,Win95 是 设计此文件格式时的主要操作系统以及 仅支持强制文件锁定。所有现代操作系统 我们知道支持咨询文件锁定,因此锁定字节页面是 实际上不再需要,但保留以向后兼容。
1.5. 自由列表
数据库文件可能包含一个或多个不在 积极使用。例如,当信息出现时,可能会出现未使用的页面 将从数据库中删除。未使用的页面存储在自由列表中 并在需要其他页面时重复使用。
freelist 被组织为自由列表主干页面的链表 每个主干页面都包含零个或多个 freelist 的页码 叶页。
freelist 主干页面由一个 4 字节大端整数数组组成。 数组的大小与可用空间中容纳的整数一样多 一页。最小可用空间为 480 字节,因此数组将始终 长度至少为 120 个条目。freelist 主干上的第一个整数 page 是列表中下一个 freelist 主干页面的页码或零 如果这是最后一个 freelist 主干页面。自由列表上的第二个整数 主干页是要遵循的叶页指针的数量。 调用自由列表主干页 L 上的第二个整数。 如果 L 大于零,则数组索引介于 2 和 之间的整数 L+1(含)包含自由列表叶页的页码。
自由列表叶页不包含任何信息。SQLite 避免读取或 编写 freelist 叶页以减少磁盘 I/O。
3.6.0 之前的 SQLite 版本中的错误 (2008-07-16) 导致数据库 如果 Freelist 主干页面中的最后 6 个条目中的任何一个被报告为损坏 数组包含非零值。较新版本的 SQLite 没有 这个问题。但是,较新版本的 SQLite 仍然避免使用 Freelist 主干页面数组中的最后六个条目,以便该数据库 由较新版本的 SQLite 创建的文件可以由旧版本读取 的 SQLite。
自由列表页数存储为 4 字节大端整数 在数据库标头中,与文件开头的偏移量为 36。 数据库头还存储第一个自由列表主干的页码 page 作为 4 字节大端整数,从开头偏移量为 32 的文件。
1.6. B树页面
b 树算法提供具有唯一和 面向页面的存储设备上的排序键。 有关 b 树的背景信息,请参阅 b 树。 Knuth,《计算机编程的艺术》,第 3 卷“排序” 和搜索“,第 471-479 页。b 树的两种变体由 SQLite的。“表 b 树”使用 64 位有符号整数键并存储 叶子中的所有数据。“索引 b 树”使用任意键并存储 no 根本没有数据。
b 树页可以是内页,也可以是叶页。 叶页包含键,如果是表,则包含每个键 b 树 键具有关联的数据。内页包含 K 键以及指向子 b 树页面的 K+1 指针。 内部 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 树中的所有页面都属于同一类型:任一表 或索引。数据库文件中有一个表 b 树 对于数据库架构中的每个 RowID 表,包括系统表 比如sqlite_schema。有一个索引 b 树 在架构中每个索引的数据库文件中,包括隐含索引 由唯一性约束创建。没有与虚拟表关联的 b 树。特定的虚拟表实现可能会使用 用于存储的影子表,但这些影子表将具有单独的 数据库架构中的条目。没有 ROWID 表,请使用索引 b 树 而不是一个表 b 树,所以有一个 在数据库文件中为每个 WITHOUT ROWID 表编制索引 b 树。 与sqlite_schema表对应的 b 树始终是表 b-tree 并且始终具有 1 的根页。 sqlite_schema表包含每隔一个的根页码 数据库文件中的表和索引。
表 b 树中的每个条目都由一个 64 位有符号整数键组成 以及最多 2147483647 字节的任意数据。(表 b 树的键 对应于 b 树实现的 SQL 表的 rowid。 内部桌子 b 树只装钥匙和指向儿童的指针。 所有数据都包含在表 b 树叶中。
索引 b 树中的每个条目都由一个任意键 up 组成 长度为 2147483647 字节,没有数据。
将单元格的“有效负载”定义为任意长度部分 的细胞。对于索引 b 树,键的长度始终是任意的 因此,有效载荷是关键。没有任意长度的元素 在内部表 B 树页面的单元格中,因此这些单元格没有 有效载荷。表 b 树叶页包含任意长度的内容和 因此,对于这些页面上的单元格,有效负载是内容。
当单元的有效载荷大小超过某个阈值(至 稍后定义),则仅有效负载的前几个字节 存储在B树页面,余额存储在链表中 的内容溢出页面。
b树页面按以下顺序划分为多个区域:
- 100 字节数据库文件头(仅在第 1 页上找到)
- 8 或 12 字节的 b 树页眉
- 单元格指针数组
- 未分配的空间
- 单元格内容区域
- 保留区域。
100 字节的数据库文件头仅在第 1 页找到,即 始终是表 B 树页面。数据库文件中的所有其他 b 树页 省略此 100 字节标头。
保留区域是每个末尾的未使用空间区域 页面(锁定页面除外),扩展程序可以使用它来保留每个页面 信息。保留区域的大小由一个字节决定 在数据库文件头的偏移量为 20 处找到的无符号整数。 保留区域的大小通常为零。
对于叶页,b 树页眉的大小为 8 个字节,而叶页的页眉大小为 12 个字节 内页的字节。页眉中的所有多字节值 是大端的。 b-tree 页眉由以下字段组成:
B-tree 页眉格式
偏移 | 大小 | 说明 |
---|---|---|
0 | 1 | 偏移量为 0 处的单字节标志,指示 b 树页面类型。
|
1 | 2 | 偏移量 1 处的双字节整数给出了 页面上的第一个 freeblock,如果没有 freeblocks,则为零。 |
3 | 2 | 偏移量 3 处的双字节整数给出页面上的单元格数。 |
5 | 2 | 偏移量 5 处的双字节整数指定单元格内容的开始 面积。此整数的零值被解释为 65536。 |
7 | 1 | 偏移量 7 处的一字节整数给出了自由碎片的数量 单元格内容区域中的字节。 |
8 | 4 | 偏移量 8 处的四字节页码是最右边的指针。这 value 仅出现在内部 B 树页面的标题中,并且从 所有其他页面。 |
b 树页面的单元格指针数组紧跟在 b 树之后 页眉。设 K 是 btree 上的单元格数。单元格指针 数组由单元格内容的 K 个 2 字节整数偏移量组成。这 单元格指针按键顺序排列,最左边的单元格(具有 最小的键)和最右边的单元格(具有最大键的单元格) key)最后。
单元格内容存储在 b 树页面的单元格内容区域中。 SQLite努力将单元格放置在b树页面的末尾,因为 它可以,以便为单元格指针数组的未来增长留出空间。 最后一个单元格指针数组条目和 第一个单元格是未分配的区域。
如果页面不包含单元格(仅适用于根页面) 不包含行的表),则偏移量为 单元格内容区域将等于页面大小减去保留空间的字节数。 如果数据库使用 65536 字节的页面大小,并且保留空间为零 (保留空间的通常值),然后单元格内容偏移量 空页希望为 65536。 但是,该整数太大,无法存储在 2 字节无符号整数,因此在其位置使用值 0。
自由块是一种结构,用于识别其中未分配的空间 B 树页面。Freeblocks被组织成一个链。前 2 个字节 freeblock 是一个 big-endian 整数,它是 b 树页面中的偏移量 链中的下一个自由区块,如果自由区块是最后一个自由区块,则为零 链条。每个自由块形式的第三个和第四个字节 一个大端整数,其大小为 freeblock 的大小(以字节为单位),包括 4 字节标头。Freeblocks 始终按顺序连接 增加抵消量。b-tree 页眉的第二个字段是 第一个自由块的偏移量,如果 页。在格式良好的 b 树页面中,始终至少有一个单元格 在第一个 freeblock 之前。
一个 freeblock 至少需要 4 个字节的空间。如果有一个隔离的 单元格内容区域中 1、2 或 3 个未使用的字节组,这些字节 组成一个片段。存储所有片段中的总字节数 在 B 树页眉的第五个字段中。在格式良好的 b 树页面中, 片段中的总字节数不得超过 60 个。
b 树页面上的可用空间总量由 未分配的区域加上所有自由块的总大小加上 碎片可用字节数。SQLite可能会不时重组 一个 B 树页面,这样就没有 freeblocks 或 fragment 字节,所有 未使用的字节包含在未分配的空间区域中,并且所有 单元格在页面末尾紧密包装。这叫做 对 B 树页面进行“碎片整理”。
可变长度整数或“varint”是静态霍夫曼编码 的 64 位二进制补码整数,为小正数使用更少的空间 值。 变量的长度介于 1 到 9 个字节之间。变量由以下任一部分组成 零个或多个字节,其中设置了高阶位,后跟一个字节 高阶位为clear,或9个字节,以较短者为准。 前 8 个字节中每个字节的下 7 位和 第九个字节用于重建 64 位二进制补码整数。 Varint 是 big-endian:取自 varint 的较早字节的位 比从后面的字节中获取的位更重要。
单元格的格式取决于单元格的 b 树页面类型 出现在。下表显示了单元格的元素,在 出现顺序,适用于各种 B 树页面类型。
表 B 树叶单元格(标题 0x0d):
- 一个变量,它是有效负载的总字节数,包括 溢出
- 作为整数键的变量,又名“rowid”"
- 有效负载的初始部分,不会溢出到溢出 页面。
- 一个 4 字节大端整数页码,用于 溢出页面列表 - 如果所有有效负载都适合 B 树页面,则省略。
表 B 树内部单元格(标题 0x05):
- 一个 4 字节的大端页码,它是左子指针。
- 作为整数键的变量
索引 B 树叶单元格(标题 0x0a):
- 一个变量,它是密钥有效负载的总字节数,包括 溢出
- 有效负载的初始部分,不会溢出到溢出 页面。
- 一个 4 字节大端整数页码,用于 溢出页面列表 - 如果所有有效负载都适合 B 树页面,则省略。
索引 B 树内部单元格(标题 0x02):
- 一个 4 字节的大端页码,它是左子指针。
- 一个变量,它是密钥有效负载的总字节数,包括 溢出
- 有效负载的初始部分,不会溢出到溢出 页面。
- 一个 4 字节大端整数页码,用于 溢出页面列表 - 如果所有有效负载都适合 B 树页面,则省略。
上述信息可以转换为表格格式,如下所示:
B-tree 单元格格式
数据类型 | 出现在... | 说明 | |||
---|---|---|---|---|---|
表叶节点类型 (0x0d) | 表内部节点类型 (0x05) | 索引叶节点类型 (0x0a) | 索引内节点类型 (0x02) | ||
4 字节整数 | ✔ | ✔ | 左边子项的页码 | ||
varint | ✔ | ✔ | ✔ | 有效负载的字节数 | |
varint | ✔ | ✔ | 罗维德 | ||
字节数组 | ✔ | ✔ | ✔ | 有效载荷 | |
4 字节整数 | ✔ | ✔ | ✔ | 第一个溢出页的页码 |
溢出到溢出页面的有效负载量还取决于 页面类型。对于以下计算,设 U 为可用大小 ,总页面大小减去 每页的末尾。设 P 为有效载荷大小。在下文中, 符号 X 表示可直接存储的最大有效载荷量 在 b 树页面上,而不会溢出到溢出页面和符号 M 上 表示必须存储在 btree 上的最小有效负载量 允许溢出前的页面。
表 B 树叶单元:
设 X 为 U-35。如果有效载荷大小 P 小于或等于 X,则 整个有效负载存储在 B-Tree 叶页上。 设 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,则 如果 K 小于 或 否则等于 X 或 M。 索引页上存储的字节数永远不会小于 M。
以下是相同计算的替代描述:
- X 是 U-35,用于表 btree 叶页或 ((U-12)*64/255)-23 用于索引页。
- M 始终为 ((U-12)*32/255)-23。
- 设 K 为 M+((P-M)%(U-4))。
- 如果 P<=X,则有效负载的所有 P 字节都直接存储在 没有溢出的 btree 页面。
- 如果 P>X 和 K<=X,则 P 的前 K 个字节存储在 btree 页面和剩余的 P-K 字节存储在溢出页面上。
- 如果 P>X 和 K>X,则 P 的前 M 字节存储在 btree 页面和剩余的 P-M 字节存储在溢出页面上。
溢出阈值旨在提供 4 用于索引 b 树,并确保足够的有效载荷 位于通常可以访问记录标头的 B 树页面上 无需咨询溢出页面。事后看来,设计师 SQLite b-tree 逻辑意识到这些阈值可能是 变得简单多了。但是,无法更改计算 而不会导致不兼容的文件格式。和当前的计算 工作得很好,即使它们有点复杂。
1.7. 单元格有效负载溢出页面
当 b-tree 单元格的有效负载对于 b 树页面来说太大时, 多余的内容会溢出到溢出页面上。溢出页面形成链接 列表。每个溢出页的前四个字节都是一个大端 整数,即链中下一页的页码,或零 对于链中的最后一页。第五个字节到最后一个可用字节 字节用于保存溢出内容。
1.8. 指针映射或 Ptrmap 页面
指针映射或 ptrmap 页面是插入到数据库中的额外页面 使auto_vacuum和incremental_vacuum模式的操作 效率更高。数据库中的其他页面类型通常具有指针 从父母到孩子。例如,内部 b 树页面包含指针 到其子 B 树页面,溢出链具有指针 从链条中的早期链接到后期链接。ptrmap 页面包含链接 信息朝着相反的方向发展,从子项到父级。
Ptrmap 页面必须存在于任何具有非零的数据库文件中 数据库标头中偏移量为 52 处的最大根 B 树页面值。 如果最大根 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 字节的 big-endian 页码。可识别五种页面类型:
- b 树根页。这 页码应为零。
- 自由列表页面。页码应为 零。
- 第一页 单元有效载荷溢出链。页码是 b 树页面 包含内容已溢出的单元格。
- 溢出链中的页面 第一页除外。页码是 溢流链。
- 非根 b 树页面。这 页码是父 B 树页面。
在任何包含 ptrmap 页的数据库文件中,所有 b 树根页 必须位于任何非根 B 树页面、单元有效负载溢出页面之前,或者 freelist 页面。此限制可确保根页面永远不会 在自动真空或增量真空期间移动。自动吸尘器 逻辑不知道如何更新sqlite_schema的root_page字段 表,因此有必要防止根页面被移动 在自动真空期间,为了保持 sqlite_schema表。根页面将移动到 数据库文件由 CREATE TABLE、CREATE INDEX、DROP TABLE 和 DROP INDEX 操作。
2. 架构层
上述文本描述了 SQLite 文件的低级方面 格式。b-tree 机制提供了一种强大而高效的 访问大型数据集。本节将介绍如何 低级 B 树层用于实现更高级别的 SQL 能力。
2.1. 记录格式
表 b 树叶页的数据和键 索引 B 树页面的特征如上所述 作为任意字节序列。 前面的讨论提到一个键比另一个键少,但是 没有定义“小于”的含义。本节将讨论 这些遗漏。
有效负载,表 b 树数据或索引 b 树键, 始终采用“记录格式”。 记录格式定义了对应的值序列 添加到表或索引中的列。记录格式指定数字 列数、每列的数据类型以及每列的内容。
记录格式广泛使用上面定义的 64 位有符号整数的可变长度整数或可变表示形式。
记录按此顺序包含标头和正文。 标头以单个变量开头,该变量确定总数 的字节数。varint 值是 字节,包括 Size varint 本身。以下尺寸变量是 一个或多个附加变量,每列一个变量。这些额外的变量 称为“序列号”编号和 根据下图确定每列的数据类型:
记录格式的串行类型代码
串行类型 | 内容大小 | 意义 |
---|---|---|
0 | 0 | Value 为 NULL。 |
1 | 1 | value 是一个 8 位二进制补码整数。 |
2 | 2 | value 是一个大端 16 位二进制补码整数。 |
3 | 3 | Value 是一个 big-endian 24 位二进制补码整数。 |
4 | 4 | Value 是一个 big-endian 32 位二进制补码整数。 |
5 | 6 | Value 是一个 big-endian 48 位二进制补码整数。 |
6 | 8 | value 是一个 big-endian 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 | Value 是文本编码中的字符串,长度为 (N-13)/2 字节。 不存储 nul 终结符。 |
标头大小变量 串行类型 varint 通常由单个字节组成。这 大型字符串和 BLOB 的串行类型 varint 可能会扩展到两个或三个 字节变量,但这是例外而不是规则。 varint 格式在编码记录标头时非常有效。
记录中每列的值紧跟在标题之后。 对于串行类型 0、8、9、12 和 13,该值为零字节 长度。如果所有列都属于这些类型,则 记录为空。
记录的值可能少于 对应表。例如,这可能发生在 ALTER TABLE 之后......ADD COLUMN SQL 语句已增加 表架构中不修改预先存在的行的列数 在表中。 使用表架构中定义的相应列的默认值填充记录末尾的缺失值。
2.2. 记录排序顺序
索引 b 树中键的顺序由 键所表示的记录。记录比较进度列 按列。从左到右检查记录的列。这 第一对不相等的列决定了相对顺序 两条记录。各列的排序顺序如下 遵循:
- NULL 值(序列号类型 0)首先排序。
- 数值(序列号类型 1 到 9)在 NULL 之后排序 并按数字顺序排列。
- 文本值(奇数序列类型 13 及更大)在数值后排序 按列排序函数确定的顺序排列的值。
- BLOB 值(甚至序列号类型 12 及更大)按顺序排在最后 由 memcmp() 确定。
为了计算,每列的整理函数是必需的 文本字段的顺序。 SQLite定义了三个内置的排序函数:
二元的 内置的 BINARY 排序规则可逐字节比较字符串 使用 memcmp() 函数 来自标准 C 库。 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 子句覆盖此内容。
2.3. SQL表的表示
数据库架构中的每个普通 SQL 表都表示在磁盘上 由表 B 树。表 b 树中的每个条目对应一行 的 SQL 表。SQL 表的 rowid 是 64 位有符号的 表 B 树中每个条目的整数键。
每个 SQL 表行的内容通过以下方式存储在数据库文件中 首先将各列中的值组合成一个字节数组 ,然后将该字节数组存储为有效负载 表 B 树中的条目。记录中值的顺序为 与 SQL 表定义中的列顺序相同。 当 SQL 表包含 INTEGER PRIMARY KEY 列(别名为 rowid)时,则 列在记录中显示为 NULL 值。SQLite 将始终使用 引用 INTEGER PRIMARY KEY 列时,表 b 树键而不是 NULL 值。
如果列的亲和力为 REAL,并且该列包含 可以在不丢失信息的情况下转换为整数的值 (如果该值不包含小数部分,并且不会太大而不能 表示为整数),则该列可以存储在记录中 作为整数。SQLite 会将值转换回浮点 从记录中提取它时的点。
2.4. WITHOUT ROWID表的表示
如果使用 “WITHOUT ROWID” 子句创建 SQL 表 在其 CREATE TABLE 语句的末尾,则该表是 WITHOUT ROWID 表,并使用不同的磁盘表示形式。A 不带 ROWID 表使用索引 B 树而不是表 B 树进行存储。 WITHOUT ROWID b 树中每个条目的键都是一条记录 的列 PRIMARY KEY 后跟 桌子。主键列按其显示顺序显示 在 PRIMARY KEY 子句中声明,其余列出现在 它们在 CREATE TABLE 语句中出现的顺序。
因此,WITHOUT ROWID 表的内容编码是相同的 作为普通 rowid 表的内容编码,除了 重新排列列的顺序,以便 PRIMARY KEY 列出现 首先,内容用作索引 B 树中的键,而不是 而不是作为表 b 树中的数据。 具有 REAL 亲和力的列的特殊编码规则 应用于 WITHOUT ROWID 表的方式与应用于 rowid 表相同。
2.4.1. 禁止 PRIMARY KEY 中的冗余列 的 WITHOUT ROWID 表
如果 WITHOUT ROWID 表的 PRIMARY KEY 使用相同的列 多次使用相同的排序顺序,然后是第二个和 该列在 PRIMARY KEY 定义中的后续出现项为 忽视。例如,以下 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 表,其中包含 两个 PRIMARY KEY 列,“a”和“c”,按此顺序排列,后跟 两个数据列“B”和“D”,也按此顺序排列。
2.5. SQL索引的表示
每个 SQL 索引,是否通过 CREATE INDEX 语句显式声明 或由 UNIQUE 或 PRIMARY KEY 约束暗示,对应于 数据库文件中的索引 B 树。 索引 b 树中的每个条目对应于 关联的 SQL 表。 索引 b 树的关键是 由正在编制索引的列组成的记录,后跟 对应表行的键。对于普通表,行键为 rowid,对于 WITHOUT ROWID 表,行键是 PRIMARY KEY。 因为表中的每一行都有一个唯一的行键, 索引中的所有键都是唯一的。
在普通索引中,行与行之间存在一对一的映射 表以及与该表关联的每个索引中的条目。 但是,在部分索引中,索引 b 树仅包含条目 对应于表行,其中 WHERE 子句表达式在 CREATE INDEX 语句为 true。 索引树和表 b 树中的相应行共享相同的 rowid 或主键值,并包含所有索引列的相同值。
2.5.1. 抑制 WITHOUT ROWID 二级索引中的冗余列
在 WITHOUT ROWID 表的索引中,如果 PRIMARY KEY 的列 也是索引中的一列,并且具有匹配的排序顺序,则 索引列在 table-key 后缀中不会重复 索引记录的末尾。例如,请考虑以下 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 列已经出现在 索引,则从键后缀中省略它。
在极端情况下,被索引的列覆盖所有列 的 PRIMARY KEY,索引将仅包含以下列 索引。上面的 ex25acde 示例演示了这一点。每个条目 EX25ACDE 索引仅包含列 A、C、D 和 E,其中 次序。
ex25ae 中的每一行包含五列:a、e、d、c、a。“a” 列重复,因为第一次出现的“a”具有整理 函数为“nocase”,第二个具有“binary”的整理序列。 如果“a”列不重复,并且表包含两个或更多 具有相同“e”值的条目,其中“a”仅在大小写上有所不同,则 所有这些表条目将对应于 索引,这将破坏表之间的一对一对应关系 和索引。
禁止显示索引键后缀中的冗余列 条目仅出现在 WITHOUT ROWID 表中。在一个普通的吵闹表中, 索引条目始终以 rowid 结尾,即使 INTEGER PRIMARY KEY 列是要编制索引的列之一也是如此。
2.6. SQL 数据库架构的存储
数据库文件的第 1 页是表 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表 除了 application- 之外,还包含内部架构对象的条目 和程序员定义的对象。
sqlite_schema.type 列将为 1 以下文本字符串:“table”、“index”、“view”或“trigger” 根据定义的对象类型。使用“table”字符串 适用于普通表和虚拟表。
sqlite_schema.name 列将保存对象的名称。表的 UNIQUE 和 PRIMARY KEY 约束导致 SQLite 创建名称形式为“sqlite_autoindex_TABLE_N”的内部索引 其中 TABLE 替换为包含 constraint 和 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列是名称列的副本。对于索引,tbl_name 是索引的表的名称。对于触发器,tbl_name 列存储导致触发的表或视图的名称 开火。
sqlite_schema.rootpage 列存储根目录的页码 表和索引的 B 树页面。对于定义视图、触发器、 和虚拟表,rootpage 列为 0 或 NULL。
sqlite_schema.sql 列存储描述 对象。此 SQL 文本是 CREATE TABLE、CREATE VIRTUAL TABLE、CREATE INDEX、CREATE VIEW 或 CREATE TRIGGER 语句,如果根据这些语句进行评估 当数据库文件是数据库连接的主数据库时,它将重新创建对象。文本通常是原件的副本 语句用于创建对象,但应用了规范化 文本符合以下规则:
- 开头的 CREATE、TABLE、VIEW、TRIGGER 和 INDEX 关键字 的语句转换为全部大写字母。
- 如果 TEMP 或 TEMPORARY 关键字出现在 初始 CREATE 关键字。
- 出现在 正在创建的对象将被删除。
- 前导空格被移除。
- 前两个关键字后面的所有空格都转换为单个 空间。
sqlite_schema.sql列中的文本是原件的副本 创建对象的 CREATE 语句文本,但规范化为 如上所述,并由后续的 ALTER TABLE 语句修改。 对于以下内部索引,sqlite_schema.sql为 NULL 由 UNIQUE 或 PRIMARY KEY 约束自动创建。
2.6.1. 模式表的替代名称
名称“sqlite_schema”不会出现在文件格式的任何位置。 该名称只是数据库实现使用的约定。 由于历史和运营方面的考虑, “sqlite_schema”表有时也可以由以下之一调用 以下别名:
- sqlite_master
- sqlite_temp_schema
- sqlite_temp_master
因为架构表的名称不会出现在 文件格式,如果出现以下情况,则数据库文件的含义不会改变 应用程序选择通过以下之一来引用架构表 这些替代名称。
2.6.2. 内部模式对象
除了 应用程序和/或开发人员使用 CREATE 语句 SQL、 sqlite_schema表可能包含零个或多个内部架构对象的条目,这些条目由 SQLite 为其创建 自己内部使用。内部架构对象的名称 始终以“sqlite_”和任何表、索引、视图或触发器开头 其名称以“sqlite_”开头是一个内部架构对象。 SQLite 禁止应用程序创建名称以 EACH 开头的对象 带有“sqlite_”。
SQLite 使用的内部架构对象可能包括以下内容:
-
名称形式为“sqlite_autoindex_TABLE_N”的索引 用于实现 UNIQUE 和 PRIMARY KEY 约束 普通表。
-
名称为“sqlite_sequence”的表,用于跟踪 表的最大历史整数主键 使用 AUTOINCREMENT。
-
名称形式为“sqlite_statN”的表,其中 N 为整数。 此类表存储由 ANALYZE 命令收集的数据库统计信息,并由查询规划器用于帮助确定最佳 用于每个查询的算法。
新的内部架构对象名称,始终以“sqlite_”开头, 可能会在将来的版本中添加到 SQLite 文件格式中。
2.6.3. sqlite_sequence表
sqlite_sequence表是用于帮助实现 AUTOINCREMENT 的内部表。自动创建sqlite_sequence表 每当任何具有 AUTOINCREMENT 整数主的普通表时 密钥已创建。创建后,sqlite_sequence表将存在于 永远sqlite_schema桌子;它不能被丢弃。 sqlite_sequence表的架构为:
CREATE TABLE sqlite_sequence(name,seq);
sqlite_sequence表中有一个行对应每个普通 使用 AUTOINCREMENT 的表。表的名称(如 sqlite_schema.name) 位于 sqlite_sequence.name 字段中,并且插入该表的最大整数主键为 在 sqlite_sequence.seq 字段中。 自动生成的 AUTOINCREMENT 的新整数主键 表保证大于 sqlite_sequence.seq 字段 那张桌子。 如果 AUTOINCREMENT 表的 sqlite_sequence.seq 字段已位于 然后,最大整数值 (9223372036854775807) 尝试添加新的 具有自动生成的整数主表的行将失败 出现SQLITE_FULL错误。 如果需要,sqlite_sequence.seq 字段会在以下情况下自动更新 新条目将插入到 AUTOINCREMENT 表中。 自动删除 AUTOINCREMENT 表的sqlite_sequence行 当表格被丢弃时。 如果 AUTOINCREMENT 表的sqlite_sequence行不存在,则在以下情况下 更新 AUTOINCREMENT 表,然后创建新的sqlite_sequence行。 如果 AUTOINCREMENT 表的 sqlite_sequence.seq 值是手动的 设置为整数以外的其他内容,并且随后尝试 插入 AUTOINCREMENT 表或更新 AUTOINCREMENT 表,则行为未定义。
允许应用程序代码修改sqlite_sequence表,以添加 新建行,删除行或修改现有行。但是,应用 如果 sqlite_sequence 表尚不存在,则代码无法创建该表。 应用程序代码可以从sqlite_sequence表中删除所有条目, 但应用程序代码不能删除sqlite_sequence表。
2.6.4. sqlite_stat1表
sqlite_stat1是由 ANALYZE 命令创建的内部表 并用于保存有关表和索引的补充信息,这些信息 查询规划器可用于帮助它找到执行查询的更好方法。 应用程序可以更新、删除、插入或删除sqlite_stat1 表,但不能创建或更改sqlite_stat1表。 sqlite_stat1表的架构如下所示:
CREATE TABLE sqlite_stat1(tbl,idx,stat);
每个索引通常有一行,索引由 sqlite_stat1.idx 列中的 name。sqlite_stat1.tbl 列是 索引所属的表的名称。在每一行中, sqlite_stat.stat 列将是 由整数列表后跟零或多个的字符串组成 参数。其中的第一个整数 list 是索引中的近似行数。(数量 索引中的行数与表中的行数相同, 部分索引除外。 第二个整数是索引中的近似行数 在索引的第一列中具有相同的值。第三个 integer 是索引中具有 前两列的值相同。第 N 个整数(对于 N>1) 是 对前 N-1 列具有相同值的索引。为 一个 K 列索引,则统计列中将有 K+1 个整数。如果 索引是唯一的,则最后一个整数将为 1。
可以选择跟随统计列中的整数列表 by 参数,每个参数都是一系列非空格字符。 所有参数前面都有一个空格。 无法识别的参数将被静默忽略。
如果存在“unordered”参数,则查询规划器假定 索引是无序的,并且不会将索引用于范围查询 或用于排序。
“sz=NNN”参数(其中 NNN 表示 1 位或更多位的序列) 表示表的所有记录的平均行大小或 index 是每行的 NNN 字节数。SQLite 查询规划器可能会使用 “sz=NNN”令牌提供的估计行大小信息 以帮助它选择需要较少磁盘 I/O 的较小表和索引。
sqlite_stat1.stat 字段上存在“noskipscan”令牌 的索引可防止该索引与跳过扫描优化一起使用。
将来可能会将新的文本标记添加到统计列的末尾 对 SQLite 的增强。为了兼容性,末尾有无法识别的令牌 的 stat 列将被静默忽略。
如果 sqlite_stat1.idx 列为 NULL,则 sqlite_stat1.stat 列包含一个整数,它是 表中由 sqlite_stat1.tbl 标识的行。 如果 sqlite_stat1.idx 列与 sqlite_stat1.tbl 相同 列,则该表是 WITHOUT ROWID 表和 sqlite_stat1.stat 字段包含有关实现 没有 ROWID 表。
2.6.5. sqlite_stat2表
仅创建sqlite_stat2,并且仅在编译 SQLite 时使用。 如果 SQLITE_ENABLE_STAT2,如果 SQLite 版本号介于 3.6.18 (2009-09-11) 和 3.7.8 (2011-09-19)。 sqlite_stat2表既不被任何人读取也不写入 3.6.18 之前的 SQLite 版本,以及 3.7.8 之后的版本。 sqlite_stat2表包含其他信息 关于索引中键的分布。 sqlite_stat2表的架构如下所示:
CREATE TABLE sqlite_stat2(tbl,idx,sampleno,sample);
每个列中的 sqlite_stat2.idx 列和 sqlite_stat2.tbl 列 sqlite_stat2 表的行标识该行描述的索引。 sqlite_stat2中通常有 10 行 每个索引的表。
具有 sqlite_stat2.sampleno 的索引的sqlite_stat2条目 0 和 9(含 0 和 9)是 索引沿索引均匀分布的点。 设 C 为索引中的行数。 然后,采样行由下式给出
行数 = (i*C*2 + C)/20
上一个表达式中的变量 i 在 0 和 9 之间变化。 从概念上讲,索引空间分为 10 个均匀的桶,样品是每个桶的中间一行。
此处记录了sqlite_stat2的格式,以供遗留参考。 最新版本的 SQLite 不再支持 sqlite_stat2 和 sqlite_stat2表(如果存在)将被忽略。
2.6.6. sqlite_stat3表
仅当编译 SQLite 时才使用sqlite_stat3 具有 SQLITE_ENABLE_STAT3 或 SQLITE_ENABLE_STAT4,并且 SQLite 版本号为 3.7.9 (2011-11-01) 或更高版本。 sqlite_stat3表既不被任何人读取也不写入 低于 3.7.9 的 SQLite 版本。 如果使用 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 命令通常会生成sqlite_stat3表 包含 10 到 40 个样本,分布在 键空间,并具有较大的 nEq 值。
在格式良好的sqlite_stat3表中,任何单个的样本 索引的显示顺序必须与它们在索引中的出现顺序相同。 换言之,如果最左边列 S1 的条目在 索引 b 树比 条目包含最左边的列 S2,然后在sqlite_stat3表中, 示例 S1 的 rowid 必须小于示例 S2。
2.6.7. sqlite_stat4表
仅创建sqlite_stat4,并且仅在编译 SQLite 时使用。 如果 SQLite 版本号为 SQLITE_ENABLE_STAT4,则为 3.8.1 (2013-10-17) 或更高版本。 sqlite_stat4表既不被任何人读取也不写入 3.8.1 之前的 SQLite 版本。 sqlite_stat4表包含其他信息 关于索引中键的分布或 WITHOUT ROWID 表的主键中的键。 查询规划器有时可以使用 sqlite_stat4表,用于设计更好、更快的查询算法。 sqlite_stat4表的架构如下所示:
CREATE TABLE sqlite_stat4(tbl,idx,nEq,nLt,nDLt,sample);
sqlite_stat4表中通常有 10 到 40 个条目 每个可用的统计数据索引,但这些限制是 不是硬性界限。 sqlite_stat4表中各列的含义如下:
结核病: | sqlite_stat4.tbl 列包含 该行描述的索引 |
印尼达: | sqlite_stat4.idx 列包含索引的名称,该索引 行描述,或者在 WITHOUT ROWID 表的sqlite_stat4条目, 表本身的名称。 |
样本: | sqlite_stat4.sample 列包含 BLOB 以对索引列进行编码的记录格式,后跟 rowid 表的 rowid 或主键的列 对于 WITHOUT ROWID 表。 WITHOUT ROWID 表本身的 sqlite_stat4.sample BLOB 仅包含主键的列。 设 sqlite_stat4.sample blob 编码的列数为 N。 对于普通 rowid 表上的索引,N 将比数字多 1 索引的列数。 对于 WITHOUT ROWID 表上的索引,N 将是列数 indexed 加上主键中的列数。 对于 WITHOUT ROWID 表,N 将是 主键。 |
当量: | sqlite_stat4.nEq 列包含 N 个整数的列表,其中 第 K 个整数是索引中条目的近似数 其最左边的 K 列与最左边的 K 列完全匹配 的样本。 |
nLt: | sqlite_stat4.nLt 列包含 N 个整数的列表,其中 第 K 个整数是 其 K 个最左边列的总和 K 个样本最左边的列。 |
nDLt: | sqlite_stat4.nDLt 列包含 N 个整数的列表,其中 第 K 个整数是近似值 索引中在前 K 列中不同的条目数,以及 其中最左边的 K 列总小于最左边的 K 列 样品的 K 列。 |
sqlite_stat4是sqlite_stat3表的概括。这 sqlite_stat3表提供有关 index,而 sqlite_stat4 表提供有关所有列的信息 的索引。
每个索引可以有任意数量的sqlite_stat4条目。 ANALYZE 命令通常会生成sqlite_stat4表 包含 10 到 40 个样本,分布在 键空间,并具有较大的 nEq 值。
在格式良好的sqlite_stat4表中,任何单个的样本 索引的显示顺序必须与它们在索引中的出现顺序相同。 换言之,如果条目 S1 在索引 b 树中早于 条目 S2,则在sqlite_stat4表中,示例 S1 必须具有 比样本 S2 小的 rowid。
3. 回滚日志
回滚日志是与每个 SQLite 数据库关联的文件 文件,用于将数据库文件还原到其初始 在事务过程中的状态。 回滚日志文件始终位于同一 目录作为数据库 文件,与数据库文件同名,但字符串 附加“-journal”。只能有一个回滚日志 与给定数据库相关联,因此只能有一次写入 一次针对单个数据库打开事务。
如果事务由于应用程序崩溃而中止,则操作 系统崩溃,或硬件电源故障或崩溃,则数据库可能 处于不一致的状态。下次SQLite尝试打开时 数据库文件,回滚日志文件的存在将是 检测到,日志将自动回放以恢复 数据库设置为未完成事务开始时的状态。
回滚日志仅在存在且 包含有效的标头。因此,事务可以在一个中提交 三种方式:
- 可以删除回滚日志文件,
- 回滚日志文件可以截断为零长度,或者
- 回滚日志的标头可以覆盖 无效的标题文本(例如,全零)。
这三种提交事务的方式对应于 DELETE, 分别对journal_mode编译指示进行 TRUNCAT 和 PERSIST 设置。
有效的回滚日志以下列格式的标题开头:
回滚日记帐标题格式
抵消 | 大小 | 描述 |
---|---|---|
0 | 8 | 标头字符串:0xd9、0xd5、0x05、0xf9、0x20、0xa1、0x63、0xd7 |
8 | 4 | “页数” - 下一段的页数 journal,或 -1 至 表示所有内容到文件末尾 |
12 | 4 | 校验和的随机随机数 |
16 | 4 | 数据库的初始大小(以页为单位) |
20 | 4 | 写入此内容的进程假定的磁盘扇区的大小 杂志。 |
24 | 4 | 此日记中的页面大小。 |
回滚日志标题用零填充到 单个扇区(由偏移量 20 处的扇区大小整数定义)。 接头本身位于扇区中,因此如果发生断电,则 写入扇区,标题后面的信息将是 (希望)完好无损。
在页眉和零填充之后是零个或多个页面记录。每 页面记录存储数据库文件中页面内容的副本 在它被改变之前。同一页面可能不会出现多次 在单个回滚日志中。 要回滚未完成的事务,请执行流程 只需从头到尾阅读回滚日志,然后 将日志中的页面写回数据库文件中,网址为 适当的位置。
设数据库页面大小(偏移量为24处的整数值 在日志标题中)为 N。 那么页面记录的格式如下:
回滚日记页记录格式
抵消 | 大小 | 描述 |
---|---|---|
0 | 4 | 数据库文件中的页码 |
4 | N | 交易开始前页面的原始内容 |
N+4 | 4 | 校验和 |
校验和是一个无符号的 32 位整数,计算方式如下:
- 将校验和初始化为在 偏移量 12 处的日记帐标题。
- 将索引 X 初始化为 N-200(其中 N 是数据库页的大小 以字节为单位。
- 将偏移量 X 处的字节解释为 8 位无符号整数 并将该整数的值添加到校验和中。
- 从 X 中减去 200。
- 如果 X 大于或等于零,请返回步骤 3。
校验和值用于防止不完整的写入 电源故障后的日记页记录。不同的随机随机数 每次启动交易时都会使用,以最大限度地降低风险 不成文的扇区可能偶然包含来自同一页面的数据 这是以前期刊的一部分。通过更改每个随机数 事务,磁盘上的陈旧数据仍将生成不正确的校验和 并且被高概率检测到。校验和仅使用稀疏样本 出于性能原因,数据记录中的 32 位字 - 设计研究 在 SQLite 3.0.0 的规划阶段显示 对整个页面进行校验和的性能显著降低。
让日记帐标题中偏移量 8 处的页数值为 M。 如果 M 大于零,则在 M 页记录日志文件之后 可能为零填充到扇区大小的下一个倍数和另一个 可以插入日志标题。所有日记帐标题都在同一 日志必须包含相同的数据库页面大小和扇区大小。
如果 M 在初始日记帐标题中为 -1,则页数记录 接下来是通过计算将容纳多少页记录来计算的 日志文件其余部分的可用空间。
4. 预写日志
从版本 3.7.0 (2010-07-21) 开始, SQLite支持新事务 称为“预写日志”或“WAL”的控制机制。 当数据库处于 WAL 模式时,与该数据库的所有连接都必须 使用 WAL.特定数据库将使用回滚日志 或 WAL,但不能同时使用两者。 WAL 始终与数据库位于同一目录中 文件,与数据库文件同名,但字符串 附加“-wal”。
4.1. WAL文件格式
WAL 文件由一个标头后跟零个或多个“帧”组成。 每一帧都记录了单页的修订内容 数据库文件。对数据库的所有更改都通过写入来记录 帧进入 WAL。当写入帧时,事务提交 包含提交标记。单个 WAL 可以而且通常确实记录 多个事务。定期,WAL 的内容是 在名为 “检查点”。
单个 WAL 文件可以多次重复使用。换言之, WAL 可以填满帧,然后进行检查点,然后是新的 帧可以覆盖旧帧。WAL 总是从一开始就成长 接近尾声。附加到每个帧的校验和和计数器是 用于确定 WAL 中的哪些帧有效以及哪些帧有效 是先前检查站的剩菜。
WAL 标头的大小为 32 个字节,由以下 8 个字节组成 big-endian 32 位无符号整数值:
WAL 标头格式
抵消 | 大小 | 描述 |
---|---|---|
0 | 4 | 幻数。0x377f0682或0x377f0683 |
4 | 4 | 文件格式版本。目前为3007000。 |
8 | 4 | 数据库页面大小。示例:1024 |
12 | 4 | 检查点序列号 |
16 | 4 | Salt-1:随机整数随每个检查点递增 |
20 | 4 | Salt-2:每个检查点都有不同的随机数 |
24 | 4 | 校验和-1:标头前 24 个字节的校验和的第一部分 |
28 | 4 | 校验和-2:标头前 24 个字节的校验和的第二部分 |
紧跟在 wal-header 之后的是零帧或更多帧。每 Frame 由一个 24 字节的帧头和一个页面大小的字节组成 的页面数据。帧头是 6 个 big-endian 32 位无符号 整数值,如下所示:
WAL 帧头格式
抵消 | 大小 | 描述 |
---|---|---|
0 | 4 | 页码 |
4 | 4 | 对于提交记录,数据库文件的大小(以页为单位) 提交后。对于所有其他记录,为零。 |
8 | 4 | 从 WAL 标头复制的 Salt-1 |
12 | 4 | 从 WAL 标头复制的 Salt-2 |
16 | 4 | 校验和-1:累积校验和,包括此页 |
20 | 4 | 校验和-2:累积校验和的后半部分。 |
当且仅当以下条件为 真:
-
帧头中的 salt-1 和 salt-2 值匹配 wal-header 中的 salt 值
-
帧标头最后 8 个字节中的校验和值 与连续计算的校验和完全匹配 WAL 标头的前 24 个字节和前 8 个字节,以及 所有帧的内容 直到并包括当前帧。
4.2. 校验和算法
校验和是通过将输入解释为 偶数个无符号 32 位整数:x(0) 到 x(N)。 如果 32 位整数是 big-endian,则 WAL 标头的前 4 个字节中的幻数为 0x377f0683,并且 如果幻数为 0x377f0682,则整数为 little-endian。 校验和值始终存储在帧标头中 big-endian 格式,无论使用哪个字节顺序进行计算 校验和。
校验和算法仅适用于 长度为 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 省略了最终项。
4.3. 检查点算法
在检查点上,首先使用 VFS 的 xSync 方法。 然后,将 WAL 的有效内容传输到数据库文件中。 最后,使用另一个数据库将数据库刷新到持久性存储 xSync 方法调用。 xSync 操作充当写入屏障 - 启动所有写入 在 xSync 必须完成之前,在 xSync 开始。
检查点无需运行即可完成。可能是一些 读取器仍在使用包含的数据的旧事务 在数据库文件中。在这种情况下,传输较新的内容 从 WAL 文件到数据库的事务将删除内容 从仍在使用旧事务的读者中走出来。为了避免这种情况, 只有当所有读者都使用 WAL 中的最后一笔交易。
4.4. WAL复位
在完成检查点后,如果事务中没有其他连接 使用 WAL,则后续写入事务可以 从头开始覆盖 WAL 文件。这称为“重置 沃尔“。在第一个新的开始 写入事务,WAL 标头 salt-1 值递增 并且 SALT-2 值是随机的。盐的这些变化使盐失效 WAL 中已检查但尚未检查的旧帧 覆盖,并防止它们再次被检查。
WAL 文件可以选择在重置时被截断,但不必如此。 如果 WAL 没有被截断,性能通常会好一点,因为 文件系统覆盖现有文件的速度通常比它们快 将增长一个文件。
4.5. 阅读器算法
要从数据库中读取页面(称为页码 P),阅读器 首先检查 WAL 以查看它是否包含页 P。如果是这样,那么 页 P 的最后一个有效实例,后跟提交帧 或者是提交帧本身成为读取的值。如果 WAL 不包含有效的页面 P 副本,并且是提交 frame 或后跟一个提交帧,然后从中读取第 P 页 数据库文件。
要启动读取事务,读取器记录值的数量 WAL 中的帧作为“mxFrame”。(更多详情) 读取器使用此记录的 mxFrame 值 对于所有后续读取操作。可以附加新交易 添加到 WAL,但只要读取器使用其原始 mxFrame 值 并忽略随后附加的内容,读者将看到一个 从单个时间点对数据库进行一致的快照。 此技术允许多个并发读取器查看不同的 同时版本的数据库内容。
前几段中的读取器算法工作正常,但是 因为页面 P 的框架可以出现在 WAL 中的任何位置,所以 读取器必须扫描整个 WAL 以查找第 P 页帧。如果 WAL 很大(通常为数兆字节),扫描速度可能很慢, 并且读取性能受到影响。为了克服这个问题,一个单独的 维护称为 wal-index 的数据结构以加快 搜索特定页面的框架。
4.6. WAL-Index 格式
从概念上讲,wal-index 是共享内存,尽管当前 VFS 实现使用内存映射文件作为操作系统 可移植性。内存映射 文件与数据库位于同一目录中,并具有相同的名称 作为附加了“-shm”后缀的数据库。因为 wal-index 是共享内存,当客户端位于不同的机器上时,SQLite 不支持网络文件系统上的 journal_mode=WAL,因为 数据库的所有客户端必须能够共享相同的内存。
wal-index 的目的是快速回答这个问题:
给定页码 P 和最大 WAL 帧索引 M, 返回不超过 M 的第 P 页的最大 WAL 帧索引, 或者,如果页面 P 没有不超过 M 的帧,则返回 NULL。
上一段中的 M 值是“mxFrame”值 在第 4.4 节中定义,在开头阅读 ,并定义来自 WAL 的最大帧 读者将使用。
wal 指数是瞬态的。崩溃后,wal-index 为 从原始 WAL 文件重建。VFS 是必需的 截断或将 wal-index 的标头归零,当最后一个 与它的连接关闭。因为wal指数是瞬态的,所以它可以 使用特定于体系结构的格式;它不一定是跨平台的。 因此,与存储所有值的数据库和 WAL 文件格式不同 作为大端,wal-index 将多字节值存储在本机中 主机的字节顺序。
本文档涉及数据库的持久状态 文件,并且由于 wal-index 是瞬态结构,因此无需进一步 有关 WAL 索引格式的信息将在此处提供。 有关 wal-index 格式的其他详细信息,请参见 单独的 WAL 索引文件格式文档。