SQLite 的一个显著特征是数据库由单个磁盘文件组成。这简化了 SQLite 的使用,因为移动或备份数据库就像复制单个文件一样简单。它还使 SQLite 适用于用作应用程序文件格式。但是,虽然完整的数据库存储在单个磁盘文件中,但 SQLite 在处理数据库的过程中确实使用了许多临时文件。
本文介绍了 SQLite 创建和使用的各种临时文件。它描述了文件何时创建、何时删除、用于什么目的、为什么它们很重要以及如何在创建临时文件代价高昂的系统上避免它们。
SQLite 使用临时文件的方式不被认为是 SQLite 与应用程序之间约定的部分。本文档中的信息是对 SQLite 在编写或上次更新本文档时运行方式的正确描述。但是,不能保证 SQLite 的未来版本将以相同的方式使用临时文件。未来版本的 SQLite 可能采用新的临时文件类型,并且某些当前的临时文件用途可能会在 SQLite 的未来版本中停止使用。
SQLite 目前使用九种不同的临时文件类型
后续内容将提供有关每种临时文件类型的更多信息。
回滚日志是一个临时文件,用于在 SQLite 中实现原子提交和回滚功能。(有关其工作原理的详细讨论,请参阅名为SQLite 中的原子提交的单独文档。)回滚日志始终位于与数据库文件相同的目录中,并且具有与数据库文件相同的名称,只是附加了 8 个字符“-journal”。回滚日志通常在事务首次启动时创建,并在事务提交或回滚时删除。回滚日志文件对于实现 SQLite 的原子提交和回滚功能至关重要。如果没有回滚日志,SQLite 将无法回滚未完成的事务,如果在事务过程中发生崩溃或电源故障,则整个数据库可能会在没有回滚日志的情况下损坏。
回滚日志通常分别在事务开始和结束时创建和销毁。但此规则存在例外。
如果在事务过程中发生崩溃或电源故障,则回滚日志文件将保留在磁盘上。下次另一个应用程序尝试打开数据库文件时,它会注意到存在已放弃的回滚日志(在这种情况下,我们将其称为“热日志”),并使用日志中的信息将数据库恢复到事务开始之前的状态。这就是 SQLite 如何实现原子提交。
如果应用程序使用 pragma 将 SQLite 设置为独占锁定模式
PRAGMA locking_mode=EXCLUSIVE;
SQLite 会在独占锁定模式会话中的第一个事务开始时创建一个新的回滚日志。但在事务结束时,它不会删除回滚日志。回滚日志可能会被截断,或者其标头可能会被清零(取决于您使用的 SQLite 版本),但回滚日志不会被删除。直到退出独占访问模式后才会删除回滚日志。
journal_mode pragma也会更改回滚日志的创建和删除。默认的日志模式是 DELETE,即在每个事务结束时删除回滚日志文件的默认行为,如上所述。PERSIST 日志模式放弃删除日志文件,而是用零覆盖回滚日志标头,这可以防止其他进程回滚日志,因此与删除日志文件具有相同的效果,尽管无需实际从磁盘中删除文件。换句话说,日志模式 PERSIST 表现出与 EXCLUSIVE 锁定模式中看到的相同行为。OFF 日志模式导致 SQLite 完全省略回滚日志。换句话说,如果日志模式设置为 OFF,则永远不会写入回滚日志。OFF 日志模式禁用 SQLite 的原子提交和回滚功能。当设置了 OFF 日志模式时,ROLLBACK 命令不可用。如果在使用 OFF 日志模式的事务过程中发生崩溃或电源故障,则无法恢复,并且数据库文件可能会损坏。MEMORY 日志模式会导致回滚日志存储在内存中而不是磁盘上。当日志模式为 MEMORY 时,ROLLBACK 命令仍然有效,但由于磁盘上不存在用于恢复的文件,因此在使用 MEMORY 日志模式的事务过程中发生崩溃或电源故障可能会导致数据库损坏。
当 SQLite 在WAL 模式下运行时,将使用预写日志或 WAL 文件代替回滚日志。与回滚日志一样,WAL 文件的目的是实现原子提交和回滚。WAL 文件始终位于与数据库文件相同的目录中,并且具有与数据库文件相同的名称,只是附加了 4 个字符“-wal”。当第一个数据库连接打开时创建 WAL 文件,并且通常在最后一个数据库连接关闭时删除。但是,如果最后一个连接没有干净地关闭,则 WAL 文件将保留在文件系统中,并在下次打开数据库时自动清理。
在WAL 模式下运行时,与同一数据库文件关联的所有 SQLite 数据库连接都需要共享一些用作 WAL 文件索引的内存。在大多数实现中,这是通过对为此目的创建的文件(共享内存文件)调用 mmap() 来实现的。共享内存文件(如果存在)位于与数据库文件相同的目录中,并且具有与数据库文件相同的名称,只是附加了 4 个字符“-shm”。共享内存文件仅在 WAL 模式下运行时存在。
共享内存文件不包含持久性内容。共享内存文件的唯一目的是为多个进程提供一个共享内存块,这些进程都以 WAL 模式访问同一个数据库。如果VFS能够提供访问共享内存的替代方法,则可以使用该替代方法而不是共享内存文件。例如,如果PRAGMA locking_mode设置为 EXCLUSIVE(表示只有一个进程能够访问数据库文件),则共享内存将从堆而不是从共享内存文件中分配,并且共享内存文件将永远不会创建。
共享内存文件与其关联的 WAL 文件具有相同的生命周期。当创建 WAL 文件时创建共享内存文件,当删除 WAL 文件时删除共享内存文件。在 WAL 文件恢复期间,将根据正在恢复的 WAL 文件的内容从头开始重新创建共享内存文件。
当单个事务对已使用ATTACH语句添加到单个数据库连接中的多个数据库进行更改时,超级日志文件用作原子提交过程的一部分。超级日志文件始终位于主数据库文件(主数据库文件是在原始sqlite3_open()、sqlite3_open16()或sqlite3_open_v2()调用中标识的数据库文件,该调用创建了数据库连接)的相同目录中,并带有一个随机后缀。超级日志文件包含事务期间更改的所有各种附加辅助数据库的名称。当删除超级日志文件时,多数据库事务提交。有关更多详细信息,请参阅名为SQLite 中的原子提交的文档。
如果没有超级日志,则多数据库事务上的事务提交将对每个数据库单独进行原子操作,但不会跨所有数据库进行原子操作。换句话说,如果提交在中间因崩溃或电源故障而中断,则对一个数据库的更改可能会完成,而对另一个数据库的更改可能会回滚。超级日志会导致所有数据库中的所有更改一起回滚或提交。
超级日志文件仅为涉及多个数据库文件且至少两个数据库满足以下所有要求的COMMIT操作创建
这意味着当数据库文件已关闭同步或使用 OFF、MEMORY 或 WAL 的日志模式时,SQLite 事务在电源故障时不会跨多个数据库文件进行原子操作。对于 synchronous OFF 以及 journal_modes OFF 和 MEMORY,如果事务提交因电源故障而中断,则数据库通常会损坏。对于WAL 模式,单个数据库文件在电源故障时会跨文件原子更新,但在多文件事务的情况下,某些文件可能会回滚,而其他文件在恢复电源后可能会向前滚动。
语句日志文件用于回滚较大事务中单个语句的部分结果。例如,假设 UPDATE 语句将尝试修改数据库中的 100 行。但在修改前 50 行后,UPDATE 遇到约束冲突,这应该会阻止整个语句。语句日志用于撤消前 50 行更改,以便将数据库恢复到语句开始时的状态。
语句日志仅为可能更改数据库多行且可能在触发器中遇到约束或 RAISE 异常(因此需要撤消部分结果)的 UPDATE 或 INSERT 语句创建。如果 UPDATE 或 INSERT 不包含在 BEGIN...COMMIT 中,并且在同一数据库连接上没有其他活动语句,则不会创建语句日志,因为可以使用普通的回滚日志。如果使用替代的冲突解决算法,也会省略语句日志。例如
UPDATE OR FAIL ... UPDATE OR IGNORE ... UPDATE OR REPLACE ... UPDATE OR ROLLBACK ... INSERT OR FAIL ... INSERT OR IGNORE ... INSERT OR REPLACE ... INSERT OR ROLLBACK ... REPLACE INTO ....
语句日志被赋予一个随机名称,不一定与主数据库位于同一目录,并在事务结束时自动删除。语句日志的大小与导致创建语句日志的 UPDATE 或 INSERT 语句实现的更改的大小成正比。
使用“CREATE TEMP TABLE”语法创建的表仅对数据库连接可见,该连接最初评估了“CREATE TEMP TABLE”语句。这些临时表以及任何关联的索引、触发器和视图,都集体存储在一个单独的临时数据库文件中,该文件在第一次看到“CREATE TEMP TABLE”语句时创建。此单独的临时数据库文件也具有关联的回滚日志。用于存储临时表的临时数据库文件在使用sqlite3_close()关闭数据库连接时自动删除。
临时数据库文件与使用ATTACH语句添加的辅助数据库文件非常相似,但具有一些特殊属性。临时数据库在数据库连接关闭时始终自动删除。临时数据库始终使用synchronous=OFF和journal_mode=PERSIST PRAGMA 设置。此外,临时数据库不能与DETACH一起使用,也不能让另一个进程ATTACH临时数据库。
仅当应用程序使用“CREATE TEMP TABLE”语句时,才会创建与临时数据库及其回滚日志关联的临时文件。
包含子查询的查询有时必须单独评估子查询并将结果存储在临时表中,然后使用临时表的内容来评估外部查询。我们称之为“物化”子查询。SQLite 中的查询优化器尝试避免物化,但有时它不容易避免。由物化创建的临时表都存储在各自的单独临时文件中,这些文件在查询结束时自动删除。当然,这些临时表的大小取决于子查询物化中数据量。
IN 运算符右侧的子查询通常必须物化。例如
SELECT * FROM ex1 WHERE ex1.a IN (SELECT b FROM ex2);
在上面的查询中,子查询“SELECT b FROM ex2”被评估,其结果存储在一个临时表(实际上是一个临时索引)中,该表允许人们使用简单的二分搜索来确定 ex2.b 值是否存在。构建此表后,运行外部查询,并对每个潜在的结果行进行检查,以查看 ex1.a 是否包含在临时表中。仅当检查为真时才输出该行。
为了避免创建临时表,可以将查询重写如下
SELECT * FROM ex1 WHERE EXISTS(SELECT 1 FROM ex2 WHERE ex2.b=ex1.a);
SQLite 的最新版本(版本 3.5.4 2007-12-14 及更高版本)如果 ex2.b 列上存在索引,则会自动执行此重写。
如果 IN 运算符的右侧可以是如下所示的值列表
SELECT * FROM ex1 WHERE a IN (1,2,3);
IN 运算符右侧的值列表被视为必须物化的子查询。换句话说,前面的语句就像它一样
SELECT * FROM ex1 WHERE a IN (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3);
当 IN 运算符的右侧为值列表时,始终使用临时索引来保存右侧的值。
当子查询出现在 SELECT 语句的 FROM 子句中时,也可能需要对其进行物化。例如
SELECT * FROM ex1 JOIN (SELECT b FROM ex2) AS t ON t.b=ex1.a;
根据查询,SQLite 可能需要将“(SELECT b FROM ex2)”子查询物化到临时表中,然后执行 ex1 和临时表之间的连接。查询优化器尝试通过“扁平化”查询来避免这种情况。在前面的示例中,可以扁平化查询,SQLite 会自动将查询转换为
SELECT ex1.*, ex2.b FROM ex1 JOIN ex2 ON ex2.b=ex1.a;
更复杂的查询可能会也可能不会能够使用查询扁平化来避免临时表。查询是否可以扁平化取决于诸如子查询或外部查询是否包含聚合函数、ORDER BY 或 GROUP BY 子句、LIMIT 子句等因素。查询可以和不可以扁平化的规则非常复杂,不在本文档的讨论范围之内。
SQLite 可能会使用瞬时索引来实现 SQL 语言特性,例如
每个瞬时索引都存储在自己的临时文件中。瞬时索引的临时文件在使用它的语句结束时自动删除。
SQLite 努力使用预先存在的索引来实现 ORDER BY 子句。如果合适的索引已经存在,SQLite 将遍历索引而不是底层表来提取所需的信息,从而导致行按所需的顺序输出。但如果 SQLite 找不到合适的索引,它将评估查询并将每一行存储在一个瞬时索引中,该索引的数据是行数据,键是 ORDER BY 项。查询评估完成后,SQLite 返回并从头到尾遍历瞬时索引,以便按所需的顺序输出行。
SQLite 通过按 GROUP BY 项建议的顺序对输出行进行排序来实现 GROUP BY。将每个输出行与前一个行进行比较,以查看它是否启动了一个新的“组”。按 GROUP BY 项排序的方式与按 ORDER BY 项排序的方式完全相同。如果可能,将使用预先存在的索引,但如果没有合适的索引可用,则会创建瞬时索引。
聚合查询上的 DISTINCT 关键字通过在临时文件中创建一个瞬时索引并将每个结果行存储在该索引中来实现。在计算新的结果行时,会进行检查以查看它们是否已存在于瞬时索引中,如果它们存在,则会丢弃新的结果行。
复合查询的 UNION 运算符通过在临时文件中创建一个瞬时索引并将左右子查询的结果存储在瞬时索引中来实现,丢弃重复项。在两个子查询都评估完成后,从头到尾遍历瞬时索引以生成最终输出。
复合查询的 EXCEPT 运算符通过在临时文件中创建一个瞬时索引,将左侧子查询的结果存储在此瞬时索引中,然后从瞬时索引中删除右侧子查询的结果,最后从头到尾遍历索引以获得最终输出。
复合查询的 INTERSECT 运算符通过创建两个单独的瞬时索引(每个索引都在一个单独的临时文件中)来实现。左右子查询分别评估到一个单独的瞬时索引中。然后两个索引一起遍历,并输出两个索引中都出现的条目。
请注意,复合查询的 UNION ALL 运算符本身不使用瞬时索引(当然,UNION ALL 的左右子查询可能会根据其组成方式使用瞬时索引)。
VACUUM 命令的工作原理是创建一个临时文件,然后将整个数据库重建到该临时文件中。然后将临时文件的内容复制回原始数据库文件,并删除临时文件。
由VACUUM命令创建的临时文件仅在命令本身持续期间存在。临时文件的大小不会大于原始数据库。
与事务控制相关的临时文件,即回滚日志、超级日志、预写日志 (WAL) 文件和共享内存文件,始终写入磁盘。但其他类型的临时文件可能仅存储在内存中,绝不写入磁盘。除了回滚、超级和语句日志之外,其他临时文件是否写入磁盘或仅存储在内存中取决于SQLITE_TEMP_STORE编译时参数、temp_store pragma以及临时文件的大小。
SQLITE_TEMP_STORE编译时参数是一个 #define,其值是介于 0 和 3(含)之间的整数。SQLITE_TEMP_STORE编译时参数的含义如下
SQLITE_TEMP_STORE编译时参数的默认值为 1,这意味着将临时文件存储在磁盘上,但提供使用temp_store pragma覆盖行为的选项。
temp_store pragma具有一个整数值,它也影响存储临时文件位置的决策。temp_store pragma 的值具有以下含义
temp_store pragma的默认设置为 0,这意味着遵循SQLITE_TEMP_STORE编译时参数的建议。
重申一下,SQLITE_TEMP_STORE编译时参数和temp_store pragma仅影响回滚日志和超级日志之外的临时文件。无论SQLITE_TEMP_STORE编译时参数和temp_store pragma的设置如何,回滚日志和超级日志始终写入磁盘。
SQLite 使用一个页面缓存来存储最近读取和写入的数据库页面。这个页面缓存不仅用于主数据库文件,还用于存储在临时文件中的临时索引和表。如果 SQLite 需要使用临时索引或表,并且SQLITE_TEMP_STORE 编译时参数和temp_store pragma被设置为将临时表和索引存储在磁盘上,那么这些信息最初仍然存储在页面缓存的内存中。临时文件不会被打开,信息也不会真正写入磁盘,直到页面缓存已满。
这意味着对于许多常见的用例,其中临时表和索引很小(小到足以放入页面缓存),不会创建临时文件,也不会发生磁盘 I/O。只有当临时数据变得太大而无法放入 RAM 时,信息才会溢出到磁盘。
每个临时表和索引都有自己的页面缓存,该缓存可以存储由 SQLITE_DEFAULT_TEMP_CACHE_SIZE 编译时参数确定的最大数量的数据库页面。(默认值为 500 个页面。)页面缓存中的最大数据库页面数对于每个临时表和索引都是相同的。该值在运行时或按表或按索引的基础上无法更改。每个临时文件都有自己的私有页面缓存,并具有自己的 SQLITE_DEFAULT_TEMP_CACHE_SIZE 页面限制。
创建临时文件的目录或文件夹由特定于操作系统的 VFS 决定。
在类 Unix 系统上,按照以下顺序搜索目录
在 Windows 系统上,按照以下顺序搜索文件夹
此页面上次修改于 2022-01-08 05:02:57 UTC