本文档描述并定义了自3.0.0(2004-06-18)版本以来,所有SQLite版本都使用的磁盘数据库文件格式。
SQLite数据库的完整状态通常包含在磁盘上称为“主数据库文件”的单个文件中。
在事务期间,SQLite将其他信息存储在称为“回滚日志”的第二个文件中;如果SQLite处于 WAL模式,则该文件为预写日志文件。
如果应用程序或主机在事务完成之前崩溃,则回滚日志或预写日志包含将主数据库文件还原到一致状态所需的信息。当回滚日志或预写日志包含恢复数据库状态所需的信息时,它们称为“热日志”或“热WAL文件”。热日志和WAL文件只是错误恢复方案中的一个因素,因此并不常见,但是它们是SQLite数据库状态的一部分,因此不能忽略。本文档定义了回滚日志和预写日志文件的格式,但是重点是主数据库文件。
主数据库文件由一个或多个页面组成。页面的大小是512到65536(含)之间的2的幂。同一数据库内的所有页面大小均相同。数据库文件的页面大小由2个字节的整数确定,该整数位于距数据库文件开头16字节的偏移处。
页编号以1开始的最大页数量是4294967294(2 32 - 2)。最小大小的SQLite数据库是一个512字节的页面。数据库的最大大小为2147483646页,每页65536字节,即281,474,976,579,584字节(约281 TB)。通常,SQLite在达到自己的内部大小限制之前很久就会达到基础文件系统或磁盘硬件的最大文件大小限制。
尽管已知在生产中存在数TB大小的SQLite数据库,但通常使用的SQLite数据库的大小范围从几千字节到几GB。
在任何时间点,主数据库中的每个页面都有一次使用,这是以下之一:
对主数据库文件的所有读取和写入均始于页面边界,并且所有写入均为整数大小的页面。读取通常也是页面大小的整数,一个例外是,第一次打开数据库时,数据库文件的前100个字节(数据库文件头)被读取为子页面大小单位。
在修改数据库的任何信息页面之前,该页面的原始未修改内容都将写入回滚日志中。如果事务中断并且需要回滚,则可以使用回滚日志将数据库还原到其原始状态。自由列表叶子页不包含回滚时需要还原的信息,因此,为了减少磁盘I / O,它们不会在修改之前写入日志中。
数据库文件的前100个字节构成数据库文件头。数据库文件头分为如下表所示的字段。数据库文件头中的所有多字节字段都以最高有效字节在前(big-endian)存储。
抵消 | 尺寸 | 描述 |
---|---|---|
0 | 16 | 标题字符串:“ SQLite格式3 \ 000” |
16 | 2个 | 数据库页面大小(以字节为单位)。必须是512到32768(含)之间的2的幂,或者值1代表65536的页面大小。 |
18岁 | 1个 | 文件格式写版本。1表示旧版;WAL为2 。 |
19 | 1个 | 文件格式读取版本。1表示旧版;WAL为2 。 |
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 3300。此字节序列对应于UTF-8字符串“ SQLite format 3”,包括末尾的nul终止符。
从偏移量16开始的两字节值确定数据库的页面大小。对于SQLite版本3.7.0.1(2010-08-04)和更早版本,此值解释为big-endian整数,并且必须是512到32768(含)之间的2的幂。从SQLite版本3.7.1 (2010-08-23)开始,支持65536字节的页面大小。值65536将不适合两个字节的整数,因此要指定65536字节的页面大小,偏移量16处的值为0x00 0x01。此值可以解释为big-endian 1,并且可以看作表示65536页大小的幻数。或者可以将两个字节的字段视为一个小端数字,并说它表示页面大小除以256。页面大小字段的这两种解释是等效的。
偏移量18和19处的文件格式写入版本和文件格式读取版本旨在允许在将来的SQLite版本中增强文件格式。在当前版本的SQLite中,对于回滚日志记录模式,这两个值均为1;对于WAL 日志记录模式,这两个值均为2 。如果编码为当前文件格式规范的SQLite版本遇到数据库文件,其中读取版本为1或2,但写入版本大于2,则必须将该数据库文件视为只读。如果遇到读取版本大于2的数据库文件,则无法读取或写入该数据库。
SQLite能够在每个页面的末尾留出少量额外的字节,以供扩展使用。例如,SQLite加密扩展使用这些额外的字节来存储与每个页面关联的现时和/或加密校验和。偏移量20处的1字节整数中的“保留空间”大小是每页末尾要保留的扩展空间的字节数。该值通常为0。该值可以为奇数。
数据库页面的“可用大小”是由标头中偏移量16处的2字节整数指定的页大小减去标头中偏移量20处的1字节整数中记录的“保留”空间大小。页面的可用大小可能是奇数。但是,可用大小不能小于480。换句话说,如果页面大小是512,则保留空间大小不能超过32。
最大和最小嵌入式有效负载分数和叶有效负载分数值必须分别为64、32和32。这些值最初旨在作为可调整参数,可用于修改b树算法的存储格式。但是,不支持该功能,并且当前没有将来增加支持的计划。因此,这三个字节固定为指定的值。
文件更改计数器是偏移量24处的4字节大尾数整数,在修改数据库文件后将其解锁时,该计数器将递增。当两个或多个进程正在读取同一数据库文件时,每个进程可以通过监视更改计数器来检测其他进程的数据库更改。当另一个进程修改数据库时,一个进程通常将希望刷新其数据库页面缓存,因为该缓存已过时。文件更改计数器可简化此操作。
在WAL模式下,使用wal-index检测对数据库的更改,因此不需要更改计数器。因此,在WAL模式下,每个事务上的更改计数器可能不会增加。
标头中偏移量为28的4字节big-endian整数将数据库文件的大小存储在页面中。如果此头中数据大小大小无效(请参阅下一段),则通过查看数据库文件的实际大小来计算数据库大小。较旧版本的SQLite忽略头内数据库大小,而仅使用实际文件大小。较新版本的SQLite如果可用,则使用头内数据库大小,但如果头内数据库大小无效,则使用实际大小。
标头中的数据库大小仅在非零且偏移量为24的4字节更改计数器与4字节版本有效编号完全匹配时才被认为是有效的在偏移量92处。当仅使用SQLite的最新版本3.7.0(2010-07-21)和更高版本修改数据库时,标头内数据库大小始终有效。如果SQLite的旧版本写入数据库,它将不知道更新头内数据库大小,因此头内数据库大小可能不正确。但是,SQLite的旧版本还将在偏移量92处保留版本有效号,因此它与更改计数器不匹配。因此,可以通过观察更改计数器何时与版本有效期号不匹配来检测(并忽略)无效的页眉内数据库大小。
数据库文件中未使用的页面存储在空闲列表中。偏移量32处的4字节big-endian整数存储空闲列表第一页的页号,如果空闲列表为空,则为0。偏移量为36的4字节big-endian整数存储自由列表上的页面总数。
模式cookie是偏移量40处的4字节大尾数整数,每当数据库模式更改时,该cookie就会递增。准备好的语句是针对特定版本的数据库模式进行编译的。当数据库模式更改时,必须重新准备该语句。当准备好的语句运行时,它首先检查架构cookie,以确保该值与准备该语句时的值相同;如果架构cookie已更改,则该语句将自动重新准备并重新运行,或者由于SQLITE_SCHEMA 错误而中止。
模式格式号是偏移量44处的4字节大尾数整数。模式格式号与偏移量18和19处的文件格式读取和写入版本号相似,不同之处在于模式格式号引用了高级SQL。格式化,而不是低级b树格式化。当前定义了四个模式格式编号:
默认情况下,SQLite创建的新数据库文件使用格式4。该legacy_file_format编译指示可以用来引起的SQLite通过设置使用格式1来创建新的数据库文件的格式的版本号可以以默认被制成1代替4- SQLITE_DEFAULT_FILE_FORMAT在编译时间= 1。
偏移量48处的4字节big-endian带符号整数是数据库文件在页面中建议的高速缓存大小。该值仅是一个建议,SQLite没有义务兑现它。整数的绝对值用作建议的大小。可以使用default_cache_size pragma设置建议的缓存大小。
偏移量为52和64的两个4字节的大端整数用于管理auto_vacuum和增量_vacuum模式。如果偏移量52处的整数为零,则将从数据库文件中省略指针映射(ptrmap)页面,并且不支持auto_vacuum或incremental_vacuum。如果偏移量52处的整数不为零,则它是数据库文件中最大根页的页码,数据库文件将包含ptrmap页,并且模式必须为auto_vacuum或增量_vacuum。在后一种情况下,偏移量64处的整数对于incremental_vacuum为true,对于auto_vacuum为false。如果偏移量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用于用作应用程序文件格式的数据库文件 。诸如file(1)之类的实用程序可以使用应用程序ID来确定特定的文件类型,而不仅仅是报告“ SQLite3数据库”。通过查询 SQLite源存储库中的magic.txt文件,可以看到已分配的应用程序ID的列表 。
偏移量为96的4字节大尾数整数存储最近修改数据库文件的SQLite库的 SQLITE_VERSION_NUMBER值。偏移量为92的4字节big-endian整数是存储版本号时更改计数器的值。偏移量92处的整数表示版本号对哪个交易有效,有时称为“版本有效号”。
数据库文件头的所有其他字节均保留用于将来扩展,并且必须设置为零。
锁定字节页面是数据库文件的单个页面,其中包含1073741824和1073742335(含)之间的偏移量的字节。大小小于或等于1073741824字节的数据库文件不包含锁定字节页面。大于1073741824的数据库文件仅包含一个锁定字节页面。
保留了锁定字节页面,以供操作系统特定的 VFS实现在实现数据库文件锁定原语时使用。SQLite不使用锁定字节页面。尽管特定于操作系统的VFS 实现可能会根据底层系统的需求和倾向选择在锁定字节页上读取或写入字节,但SQLite核心将永远不会读取或写入锁定字节页。SQLite内置的unix和win32 VFS实现不会写入锁定字节页,但是其他操作系统的第三方VFS实现可能会写入。
锁定字节页面的产生是由于需要支持Win95,Win95是设计此文件格式时的主要操作系统,并且仅支持强制性文件锁定。我们所知道的所有现代操作系统都支持咨询文件锁定,因此实际上不再需要锁定字节页面,但为了向后兼容而保留了该页面。
数据库文件可能包含一个或多个未处于活动使用状态的页面。例如,当从数据库中删除信息时,可能会出现未使用的页面。未使用的页面存储在空闲列表中,并且在需要其他页面时可以重新使用。
自由列表组织为自由列表主干页面的链接列表,每个主干页面均包含零个或多个自由列表叶子页面的页码。
空闲列表主干页面由4字节的big-endian整数数组组成。数组的大小是页面可用空间中可容纳的整数个数。最小可用空间为480个字节,因此数组的长度始终至少为120个条目。空闲列表主干页面上的第一个整数是列表中下一个空闲列表主干页面的页码;如果这是最后一个空闲列表主干页面,则为零。空闲列表主干页面上的第二个整数是要跟随的叶子页面指针的数量。在自由列表主干页面L上调用第二个整数。如果L大于零,则数组索引在2到L + 1(含)之间的整数包含自由列表叶子页面的页面编号。
自由列表叶子页不包含任何信息。SQLite避免读取或写入自由列表叶页,以减少磁盘I / O。
如果Freelist主干页面数组中的最后6个条目中的任何一个包含非零值,则3.6.0之前的SQLite版本(2008-07-16)中的一个错误导致数据库被报告为已损坏。较新版本的SQLite不会出现此问题。但是,较新版本的SQLite仍避免使用自由列表主干页面数组中的最后六个条目,以便较新版本的SQLite可以读取较新版本的SQLite创建的数据库文件。
空闲列表页数以4字节的big-endian整数形式存储在数据库标头中,与文件开头的偏移量为36。数据库头还将第一个空闲列表主干页的页号存储为距文件开头32的偏移量的4字节big-endian整数。
b树算法在面向页面的存储设备上提供具有唯一和有序密钥的密钥/数据存储。有关b树的背景信息,请参阅Knuth,计算机编程艺术,第3卷“排序和搜索”,第471-479页。SQLite使用b树的两种变体。“表b树”使用64位带符号整数键并将所有数据存储在叶子中。“索引b树”使用任意键,并且根本不存储任何数据。
b树页面是内部页面或叶子页面。叶子页包含键,在表b树的情况下,每个键都具有关联的数据。内部页面包含K个键以及指向子b树页面的K + 1个指针。内部b树页面中的“指针”只是子页面的32位无符号整数页面号。
内部b树页面上的键数K几乎总是至少2,并且通常远远大于2。唯一的例外是,当页面1是内部b树页面时。页面1的可用存储空间减少了100字节,这是因为该页面的开始处存在数据库头,因此有时(很少)如果页面1是内部b树页面,则最终只能容纳aa单键。在所有其他情况下,K为2或更大。K的上限是页面上适合的键数。索引b树上的大键分为溢出页面这样一来,单个键就不会使用页面上超过四分之一的可用存储空间,因此每个内部页面都能够存储至少4个键。表b树的整数键永远不会大到需要溢出的范围,因此键溢出仅发生在索引b树上。
将叶子b树的深度定义为1,并将任何内部b树的深度定义为比其任何子树的最大深度大一倍。在格式正确的数据库中,内部b树的所有子级都具有相同的深度。
在内部b树页面中,指针和键在逻辑上与两端的指针交替。(从概念上要理解上一句话-页面内键和指针的实际布局更为复杂,将在后续内容中进行描述。)同一页面内的所有键都是唯一的,并且按从左到右的升序顺序进行逻辑组织在右边。(同样,此顺序是逻辑上的,而不是物理上的。页面内键的实际位置是任意的。)对于任何键X,X左侧的指针均指所有键均小于或等于b树的页面。 X的指针是指所有键都大于X的页面。
在内部b树页面中,每个键和其紧邻左侧的指针都组合成一个称为“单元”的结构。最右边的指针被分开保存。叶子b树页面没有指针,但是它仍然使用单元结构来保存索引b树的键或表b树的键和内容。数据也包含在单元格中。
每个b树页面最多具有一个父b树页面。没有父级的b树页面称为根页面。根b树页面及其子级闭包构成一个完整的b树。有可能(而且实际上相当普遍)拥有一个完整的b树,该树包含一个既是叶子又是根的单个页面。因为有从父母到孩子的指针,所以如果只知道根页面,则可以定位完整b树的每个页面。因此,b树通过其根页号来标识。
b树页面是表b树页面或索引b树页面。每个完整b树中的所有页面都属于同一类型:表或索引。对于数据库模式中的每个rowid表,数据库文件中都有一个表b树,其中包括诸如sqlite_schema之类的系统表。在数据库文件中,模式中的每个索引都有一个索引b树,包括由唯一性约束创建的隐式索引。没有与虚拟表关联的b树 。特定的虚拟表实现可能会使用影子表进行存储,但是这些影子表在数据库架构中将具有单独的条目。 没有行表使用索引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页上找到,该页始终是表b树页。数据库文件中的所有其他b树页面都忽略了此100字节的标头。
保留区域是每页末尾(锁定页除外)上未使用空间的区域,扩展程序可以使用这些空间来保存每页信息。保留区域的大小由在数据库文件头中偏移20的一字节无符号整数确定。保留区域的大小通常为零。
b-tree页面标头的叶子页面大小为8个字节,内部页面大小为12个字节。页面标题中的所有多字节值均为big-endian。b树页面页眉由以下字段组成:
抵消 | 尺寸 | 描述 |
---|---|---|
0 | 1个 |
偏移量0处的一字节标志指示b树页面类型。
|
1个 | 2个 | 偏移量为1的两字节整数给出了页面上第一个空闲块的开始,如果没有空闲块,则为零。 |
3 | 2个 | 偏移量3处的两字节整数给出了页面上的单元格数。 |
5 | 2个 | 偏移量5处的两字节整数表示单元内容区域的开始。该整数的零值解释为65536。 |
7 | 1个 | 偏移量7处的一字节整数给出了单元内容区域内的零碎可用字节数。 |
8 | 4 | 偏移量8处的四字节页码是最右边的指针。此值仅出现在内部b树页面的页眉中,而在所有其他页面中均省略。 |
b树页面的单元指针数组紧随b树页面标题之后。令K为btree上的像元数。单元格指针数组由到单元格内容的K个2个字节的整数偏移量组成。单元格指针按键顺序排列,最左边的单元格(键最小的单元格)排在最前面,最右边的单元格(最大键的单元格)排在最后。
单元格内容存储在b树页面的单元格内容区域中。SQLite努力将单元格尽可能地放置到b树页面的末尾,以便为单元格指针数组的未来增长留出空间。最后一个单元格指针数组条目与第一个单元格的开始之间的区域是未分配的区域。
如果页面不包含任何单元格(这仅适用于不包含行的表的根页面),则单元格内容区域的偏移量将等于页面大小减去保留空间的字节数。如果数据库使用65536字节的页面大小,且保留空间为零(保留空间的通常值),则空页面的单元格内容偏移量应为65536。但是,该整数太大,无法存储在2字节无符号整数,因此在其位置使用0值。
freeblock是一种用于标识b树页面内未分配空间的结构。Freeblocks是按链组织的。一个自由块的前2个字节是一个大端整数,它是链中下一个自由块的b树页面中的偏移量;如果该自由块是链中的最后一个字节,则为零。每个空闲块的第三个和第四个字节形成一个大端整数,它是空闲块的大小(以字节为单位),包括4字节的标头。自由块始终按偏移量增加的顺序进行连接。b树页面页眉的第二个字段是第一个空闲块的偏移量;如果页面上没有空闲块,则为零。在结构良好的b树页面中,第一个空闲块之前将始终至少有一个单元格。
一个freeblock至少需要4个字节的空间。如果在单元内容区域中有一个隔离的1、2或3个未使用字节的组,则这些字节构成一个片段。所有片段中的字节总数存储在b树页标题的第五个字段中。在格式良好的b树页面中,片段中的字节总数不得超过60。
b树页面上的可用空间总量由未分配区域的大小加上所有空闲块的总大小加上碎片化的空闲字节数组成。SQLite可能会不时重组b树页面,以便没有空闲块或片段字节,所有未使用的字节都包含在未分配的空间区域中,并且所有单元格都紧紧地包装在页面末尾。这称为对b树页面进行“碎片整理”。
可变长度整数或“ varint”是64位二进制补码整数的静态霍夫曼编码,对于较小的正值使用较少的空间。varint的长度在1到9个字节之间。varint包含零个或多个具有高位位的字节,后跟具有高位位清零的单个字节或九个字节(以较短者为准)。前八个字节中每个字节的低七位和第九个字节中的所有8位用于重构64位二进制补码整数。Varint是big-endian:从varint的前一个字节获取的位比从后一个字节获取的位更重要。
单元格的格式取决于该单元格出现在哪种b树页面上。下表按外观顺序显示了各种b树页面类型的单元格元素。
表B-树叶子单元格(标题0x0d):
表B树内部单元格(标题0x05):
索引B树叶子单元格(标头0x0a):
索引B树内部单元格(标头0x02):
上面的信息可以重新铸成表格格式,如下所示:
数据类型 | 出现在... | 描述 | |||
---|---|---|---|---|---|
桌叶(0x0d) | 桌子内部(0x05) | 索引叶(0x0a) | 内部索引(0x02) | ||
4字节整数 | ✔ | ✔ | 左孩子的页码 | ||
瓦林特 | ✔ | ✔ | ✔ | 有效负载字节数 | |
瓦林特 | ✔ | ✔ | 罗威德 | ||
字节数组 | ✔ | ✔ | ✔ | 有效载荷 | |
4字节整数 | ✔ | ✔ | ✔ | 第一个溢出页的页码 |
溢出到溢出页面上的有效负载量还取决于页面类型。对于以下计算,令U为数据库页面的可用大小,总页面大小减去每个页面末尾的保留空间。令P为有效载荷大小。在下文中,符号X表示可以直接存储在b树页面上而不会溢出到溢出页面上的最大有效负载量,而符号M表示在允许溢出之前必须存储在btree页面上的最小有效负载量。 。
表B-树叶子单元格:
令X为U-35。如果有效载荷大小P小于或等于X,则整个有效载荷都存储在b树叶子页上。令M为((U-12)* 32/255)-23,令K为M +((PM)%(U-4))。如果P大于X,则如果K小于或等于X或M,则存储在表b树叶子页上的字节数为K。叶子页上存储的字节数不得少于M。
表B树内部单元格:
表b树的内部页没有有效负载,因此永远不会溢出任何有效负载。
索引B树叶子或内部单元格:
令X为((U-12)* 64/255)-23。如果有效载荷大小P小于或等于X,则整个有效载荷都存储在b树页面上。令M为((U-12)* 32/255)-23,令K为M +((PM)%(U-4))。如果P大于X,则如果K小于或等于X或M,则存储在索引b树页上的字节数为K。索引页上存储的字节数不得少于M。
这是相同计算的替代描述:
溢出阈值旨在为索引b树提供至少4的扇出,并确保b树页面上有足够的有效负载,从而通常可以在不查询溢出页面的情况下访问记录头。事后看来,SQLite b树逻辑的设计者意识到可以简化这些阈值。但是,如果不导致不兼容的文件格式,则无法更改计算。即使有些复杂,当前的计算也可以正常工作。
当b树单元的有效负载对于b树页面而言太大时,剩余部分会溢出到溢出页面上。溢出页面形成一个链接列表。每个溢出页的前四个字节是一个大端整数,它是链中下一页的页号,对于链中的最后一页为零。从第五个字节到最后一个可用字节用于保存溢出内容。
指针映射或ptrmap页面是插入数据库中的额外页面,以使auto_vacuum和增量_vacuum模式的操作更加有效。数据库中的其他页面类型通常具有从父级到子级的指针。例如,内部b树页面包含指向其子b树页面的指针,并且溢出链具有从链中较早的链接到后来的链接的指针。ptrmap页面包含从子级到父级的相反方向的链接信息。
Ptrmap页必须存在于数据库标头中偏移量52处具有最大非零最大根b树页面值的任何数据库文件中。如果最大的根b树页面值为零,则数据库不得包含ptrmap页面。
在具有ptrmap页的数据库中,第一个ptrmap页是第2页。一个ptrmap页由5字节条目的数组组成。令J为适合页面可用空间的5字节条目数。(换句话说,J = U / 5。)第一个ptrmap页面将包含页面3到J + 2(包括3和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 | 值是big-endian IEEE 754-2008 64位浮点数。 |
8 | 0 | 值是整数0。(仅适用于架构格式4和更高版本。) |
9 | 0 | 值是整数1。(仅适用于架构格式4和更高版本。) |
10,11 | 多变的 | 保留供内部使用。这些串行类型代码将永远不会出现在格式正确的数据库文件中,但它们可能会用于SQLite有时会为其自身使用而生成的临时和临时数据库文件中。这些代码的含义可以从一个版本的SQLite转移到下一个版本。 |
N≥12甚至 | (N-12)/ 2 | 值是一个BLOB,长度为(N-12)/ 2个字节。 |
N≥13且为奇数 | (N-13)/ 2 | 值是文本编码的字符串,长度为(N-13)/ 2个字节。不存储nul终止符。 |
标头大小的varint和串行类型的varint通常将由一个字节组成。大字符串和BLOB的串行类型varint可能会扩展到两个或三个字节的varint,但这是例外,而不是规则。varint格式在编码记录头方面非常有效。
记录中每一列的值紧随标题之后。对于串行类型0、8、9、12和13,该值的长度为零字节。如果所有列均为这些类型,则记录的主体部分为空。
一条记录的值可能少于相应表中的列数。例如,在ALTER TABLE ... ADD COLUMN SQL语句增加了表架构中的列数而不修改表中已有的行之后,可能会发生这种情况 。记录末尾的缺失值使用表模式中定义的对应列的默认值填充 。
索引b树中的键顺序由键代表的记录的排序顺序确定。逐列记录比较进度。记录的列从左到右进行检查。第一对不相等的列确定两个记录的相对顺序。各个列的排序顺序如下:
甲核对函数为每一列是必要的,以便计算文本字段的顺序。SQLite定义了三个内置的整理功能:
二进制 内置的BINARY归类使用标准C库中的memcmp()函数逐字节比较字符串。 Nocase NOCASE排序规则类似于BINARY,不同之处在于,在运行比较之前,将大写ASCII字符(“ A”至“ Z”)折叠为它们的小写字母。仅将ASCII字符进行大小写折叠。NOCASE不会实现通用unicode不区分大小写的比较。 RTRIM RTRIM类似于BINARY,只是两个字符串末尾的多余空格不会更改结果。换句话说,只要字符串的结尾处的空格数不同,它们就将相互比较。
可以使用sqlite3_create_collation()接口将其他特定于应用程序的整理功能添加到SQLite 。
所有字符串的默认整理功能是BINARY。可以使用列定义上的COLLATE子句在CREATE TABLE语句中指定表列的替代整理功能 。索引列时,默认情况下,为索引中的列使用在CREATE TABLE语句中指定的相同整理函数 ,尽管可以使用CREATE INDEX语句中的COLLATE子句将其覆盖 。
数据库模式中的每个普通SQL表在磁盘上由表b树表示。表b树中的每个条目都对应于SQL表的一行。SQL表的rowid是表b树中每个条目的64位带符号整数键。
每个SQL表行的内容存储在数据库文件中,方法是首先将各个列中的值组合成记录格式的字节数组,然后将该字节数组作为有效负载存储在表b树中的条目中。记录中的值顺序与SQL表定义中的列顺序相同。当SQL表包含 INTEGER PRIMARY KEY列(别名rowid)时,则该列在记录中显示为NULL值。当引用INTEGER PRIMARY KEY列时,SQLite将始终使用表b-tree键而不是NULL值 。
如果列的亲和力为REAL,并且该列包含可以转换为整数而不会丢失信息的值(如果该值不包含小数部分,并且也不能太大以表示为整数),则该列可能是以整数形式存储在记录中。从记录中提取值时,SQLite会将值转换回浮点。
如果在其CREATE TABLE语句的末尾使用“ WITHOUT ROWID”子句创建了一个SQL表,则该表是一个WITHOUT ROWID 表,并使用不同的磁盘表示形式。WITHOUT ROWID表使用索引b树而不是表b树进行存储。WITHOUT ROWID b树中每个条目的键都是一条记录,该记录由PRIMARY KEY的各列组成,后跟该表的所有其余列。主键列按它们在PRIMARY KEY子句中声明的顺序显示,其余列按它们在CREATE TABLE语句中出现的顺序显示。
因此,WITHOUT ROWID表的内容编码与普通rowid表的内容编码相同,除了重新排列各列的顺序以使PRIMARY KEY列排在最前面,并且该内容用作索引b树,而不是表b树中的数据。具有REAL亲和力的列的特殊编码规则适用于WITHOUT ROWID表,与处理rowid表相同。
如果WITHOUT ROWID表的PRIMARY KEY多次使用具有相同整理顺序的相同列,则将忽略PRIMARY KEY定义中该列的第二次或以后出现。例如,以下CREATE TABLE语句均指定同一张表,该表在磁盘上具有完全相同的表示形式:
创建表t1(a,b,c,d,不带行的主键(a,c)); 创建表t1(a,b,c,d,不带行的主键(a,c,a,c)); 创建表t1(a,b,c,d,不带行的主键(a,A,a,C)); 创建表t1(a,b,c,d,不带行的主键(a,a,a,a,c));
当然,上面的第一个示例是该表的首选定义。所有这些示例都创建了一个WITHOUT ROWID表,该表具有两个PRIMARY KEY列,分别为“ a”和“ c”,然后是两个数据列“ b”和“ d”,也都以此顺序。
每个SQL索引,无论是通过CREATE INDEX语句显式声明还是由UNIQUE或PRIMARY KEY约束隐式表示,都对应于数据库文件中的索引b树。索引b树中的每个条目对应于关联的SQL表中的一行。索引b树的键是一条记录,该记录由要索引的列组成,后跟相应表行的键。对于普通表,行键是rowid,对于WITHOUT ROWID表,行键是PRIMARY KEY。由于表中的每一行都有唯一的行键,因此索引中的所有键都是唯一的。
在普通索引中,表中的行与与该表关联的每个索引中的条目之间存在一对一的映射。但是,在部分索引中,索引b树仅包含与CREATE INDEX语句上的WHERE子句表达式为true的表行相对应的条目。索引树b和表b树中的相应行共享相同的rowid或主键值,并且对于所有索引列都包含相同的值。
在WITHOUT ROWID表的索引中,如果PRIMARY KEY的列也是索引中的列,并且具有匹配的排序顺序,则在索引记录末尾的表键后缀中不会重复索引的列。例如,考虑以下SQL:
在没有rowid的情况下创建表ex25(a,b,c,d,e,PRIMARY KEY(d,c,a)); 在ex25(c,e)上创建索引ex25ce; 在ex25(a,c,d,e)上创建索引ex25acde; 在ex25上创建索引ex25ae(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 列是被索引的列之一,索引条目也总是以rowid结尾。
数据库文件的第1页是表b-树的根页,其中包含一个名为“ sqlite_schema ”的特殊表。该b树被称为“模式表”,因为它存储了完整的数据库模式。sqlite_schema表的结构就像是使用以下SQL创建的:
创建表sqlite_schema( 输入文字, 命名文字, tbl_name文字, 根页整数, sql文本 );
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表中,没有用于PRIMARY KEY的sqlite_schema条目,但是为PRIMARY KEY保留了“ sqlite_autoindex_TABLE_N”名称,就好像sqlite_schema条目确实存在一样。这将影响随后的UNIQUE约束的编号。“ sqlite_autoindex_TABLE_N” ,无论是在rowid表中还是在WITHOUT ROWID表中。
sqlite_schema.tbl_name列包含与该对象关联的表或视图的名称。对于表或视图,tbl_name列是name列的副本。对于索引,tbl_name是被索引表的名称。对于触发器,tbl_name列存储导致触发器触发的表或视图的名称。
sqlite_schema.rootpage列存储表和索引的根b树页面的页码。对于定义视图,触发器和虚拟表的行,根页列为0或NULL。
sqlite_schema.sql列存储描述对象的SQL文本。该SQL文本是CREATE TABLE,CREATE VIRTUAL TABLE, CREATE INDEX, CREATE VIEW或CREATE TRIGGER语句,如果针对数据库文件进行评估(如果它是数据库连接的主数据库), 则会重新创建该对象。文本通常是用于创建对象的原始语句的副本,但是应用了规范化,因此文本符合以下规则:
sqlite_schema.sql列中的文本是创建对象的原始CREATE语句文本的副本,但如上所述和通过后续ALTER TABLE语句进行了修改的规范除外。对于由UNIQUE或PRIMARY KEY约束自动创建的内部索引,sqlite_schema.sql为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表的架构为:
创建表sqlite_sequence(name,seq);
对于每个使用AUTOINCREMENT的普通表,在sqlite_sequence表中只有一行。该表的名称(显示在sqlite_schema.name中)在sqlite_sequence.main字段中,并且有史以来最大的 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表的架构如下:
创建表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列中的整数列表可以可选地后面跟参数,每个参数都是一个非空格字符序列。所有参数前面都有一个空格。无法识别的参数将被静默忽略。
如果存在“无序”参数,则查询计划者将假定索引是无序的,并且不会将索引用于范围查询或排序。
“ sz = NNN”参数(其中NNN表示一个1或多个数字的序列)表示表或索引的所有记录的平均行大小为每行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_ENABLE_STAT2编译SQLite且SQLite版本号介于3.6.18(2009-09-11)和3.7.8(2011-09-19)之间时创建并使用。在3.6.18之前或3.7.8之后,任何版本的SQLite均无法读取或写入sqlite_stat2表。sqlite_stat2表包含有关索引内键的分布的其他信息。sqlite_stat2表的架构如下:
创建表sqlite_stat2(tbl,idx,sampleno,sample);
sqlite_stat2表的每一行中的sqlite_stat2.idx列和sqlite_stat2.tbl列标识该行描述的索引。sqlite_stat2表中通常每个索引有10行。
sqlite_stat2.sampleno在0到9之间(含0和9)的索引的sqlite_stat2条目是索引中最左键值的采样,这些采样是在沿索引均匀间隔的点处获取的。令C为索引中的行数。然后采样行由下式给出
行数=(i * C * 2 + C)/ 20
上一个表达式中的变量i在0到9之间变化。从概念上讲,索引空间分为10个统一的存储桶,样本位于每个存储桶的中间行。
sqlite_stat2的格式记录在此处,以供旧参考。SQLite的最新版本不再支持sqlite_stat2,而sqlite_stat2表(如果存在)将被忽略。
只有在使用SQLITE_ENABLE_STAT3或SQLITE_ENABLE_STAT4编译SQLite 且SQLite版本号为3.7.9(2011-11-01)或更高版本时,才使用sqlite_stat3。3.7.9之前的任何版本的SQLite均无法读取或写入sqlite_stat3表。如果使用了SQLITE_ENABLE_STAT4编译时选项,并且SQLite版本号是3.8.1(2013-10-17)或更高版本,则sqlite_stat3可能被读取但未被写入。sqlite_stat3表包含有关索引内键的分布的其他信息,查询计划人员可以使用该信息来设计更好,更快的查询算法。sqlite_stat3表的架构如下:
创建表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命令将典型地生成包含在跨越密钥空间,并与大NEQ值分布的样本10和40之间sqlite_stat3表。
在格式正确的sqlite_stat3表中,任何单个索引的样本必须按照它们在索引中出现的顺序出现。换句话说,如果索引b树中具有最左列S1的条目早于具有最左列S2的条目,则在sqlite_stat3表中,样本S1的行标识必须小于样本S2。
sqlite_stat4仅在使用SQLITE_ENABLE_STAT4编译SQLite且SQLite版本号为3.8.1(2013-10-17)或更高版本时创建并使用。在3.8.1之前的任何版本的SQLite均无法读写sqlite_stat4表。sqlite_stat4表包含有关索引中的键分布或WITHOUT ROWID表的主键中的键分布的其他信息。查询计划者有时可以使用sqlite_stat4表中的其他信息来设计更好,更快的查询算法。sqlite_stat4表的架构如下:
创建表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条目的情况下,保留表本身的名称。 |
样本: | sqlite_stat4.sample列以记录格式保存一个BLOB,该BLOB对索引列进行编码,然后对rowid表的rowid或WITHOUT ROWID表的主键的列进行编码。WITHOUT ROWID表本身的sqlite_stat4.sample BLOB仅包含主键的列。让sqlite_stat4.sample Blob编码的列数为N。对于普通rowid表上的索引,N将比索引的列数多一。对于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命令将典型地生成包含在跨越密钥空间,并与大NEQ值分布的样本10和40之间sqlite_stat4表。
在格式良好的sqlite_stat4表中,任何单个索引的样本必须按照它们在索引中出现的顺序出现。换句话说,如果索引b树中的条目S1比条目S2早,则在sqlite_stat4表中,样本S1的行ID必须小于样本S2。
回滚日志是与每个SQLite数据库文件关联的文件,其中包含用于在事务过程中将数据库文件还原到其初始状态的信息。回滚日志文件始终与数据库文件位于同一目录中,并且与数据库文件具有相同的名称,但是附加了字符串“ -journal ”。与给定数据库只能关联一个回滚日志,因此一次只能针对一个数据库打开一个写事务。
如果由于应用程序崩溃,操作系统崩溃或硬件电源故障或崩溃而导致事务中止,则数据库可能处于不一致状态。下次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 + 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个字节的校验和的第二部分 |
紧随沃尔标头之后的是零个或多个帧。每个帧包含一个24字节的帧头,后跟页面数据的页面大小字节。帧头是六个大端32位无符号整数值,如下所示:
抵消 | 尺寸 | 描述 |
---|---|---|
0 | 4 | 页码 |
4 | 4 | 对于提交记录,提交后数据库文件的大小(以页为单位)。对于所有其他记录,为零。 |
8 | 4 | 从WAL标头复制的Salt-1 |
12 | 4 | 从WAL标头复制的Salt-2 |
16 | 4 | 校验和-1:累积校验和,直至包括此页面 |
20 | 4 | Checksum-2:累积校验和的后一半。 |
当且仅当满足以下条件时,才认为该帧有效:
帧标题中的salt-1和salt-2值与wal标题中的salt值匹配
帧头的最后8个字节中的校验和值与WAL头的前24个字节和前8个字节以及直到当前帧(包括当前帧)的所有帧的内容连续计算出的校验和完全匹配。
通过将输入解释为偶数个无符号32位整数(x(0)到x(N))来计算校验和。如果WAL标头的前4个字节中的幻数为0x377f0683,则32位整数为big-endian;如果幻数为0x377f0682,则32位整数为little-endian。无论将哪个字节顺序用于计算校验和,校验和值始终以大端格式存储在帧头中。
校验和算法仅适用于长度为8个字节的倍数的内容。换句话说,如果输入是x(0)到x(N),则N必须是奇数。校验和算法如下:
s0 = s1 = 0 对于我从0到n-1的步骤2: s0 + = x(i)+ s1; s1 + = x(i + 1)+ s0; 终极 #导致s0和s1
输出s0和s1都是使用相反顺序的斐波那契权重的加权校验和。(最大斐波那契权重出现在要求和的序列的第一个元素上。)s1值覆盖该序列的所有32位整数项,而s0省略最后一项。
在检查点上,首先使用VFS的xSync方法将WAL刷新到持久性存储中。然后,将WAL的有效内容传输到数据库文件中。最后,使用另一个xSync方法调用将数据库刷新到持久性存储。xSync操作充当写障碍-在xSync开始后启动的所有写操作之前,必须先完成在xSync之前启动的所有写操作。
检查点不必运行到完成。可能某些读者仍在使用旧的事务处理数据库文件中包含的数据。在这种情况下,将WAL文件中较新事务的内容传输到数据库中,会从仍使用较旧事务的读取器下方删除内容。为避免这种情况,仅当所有阅读器都在使用WAL中的最后一个事务时,检查点才会运行到完成状态。
在完成检查点之后,如果使用WAL的事务中没有其他连接,则后续的写事务可以从头开始覆盖WAL文件。这称为“重置WAL”。在第一个新的写事务开始时,WAL标头的salt-1值增加,并且salt-2值被随机化。盐的这些更改会使WAL中已被检查点但尚未覆盖的旧框架失效,并防止它们再次被检查点。
可以选择将WAL文件在重置时截断,但不是必须的。如果不截断WAL,性能通常会稍好一些,因为文件系统覆盖现有文件的速度通常比增长文件的速度快。
要从数据库中读取一个页面(称为页面编号P),阅读器首先检查WAL以查看其是否包含页面P。如果是,则为页面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的最后一个连接关闭时,要求VFS截断wal-index的标头或将其清零。由于wal-index是瞬态的,因此可以使用特定于体系结构的格式。它不必是跨平台的。因此,与将所有值存储为big endian的数据库和WAL文件格式不同,wal-index以主机的本机字节顺序存储多字节值。
该文档与数据库文件的持久状态有关,并且由于wal-index是一个临时结构,因此此处将不提供有关wal-index格式的更多信息。关于wal-index格式的其他详细信息包含在单独的WAL-index文件格式文档中。