SQLite 使用的临时文件

、简介

SQLite的显着特征之一是数据库由单个磁盘文件组成。这简化了 SQLite 的使用,因为移动或备份数据库就像复制单个文件一样简单。它还使 SQLite 适合用作 应用程序文件格式但是,虽然完整的数据库保存在单个磁盘文件中,但 SQLite 在处理数据库的过程中确实使用了许多临时文件。

本文介绍 SQLite 创建和使用的各种临时文件。它描述了文件何时创建、何时删除、它们的用途、它们为何重要以及如何在创建临时文件成本高昂的系统上避免它们。

SQLite 使用临时文件的方式不被视为 SQLite 与应用程序签订的合同的一部分。本文档中的信息正确描述了 SQLite 在本文档编写或上次更新时的运行方式。但是不能保证未来版本的 SQLite 会以同样的方式使用临时文件。可能会采用新类型的临时文件,并且在 SQLite 的未来版本中可能会停止使用某些当前的临时文件。

2.九种临时文件

SQLite 目前使用九种不同类型的临时文件:

  1. 回滚日志
  2. 超级期刊
  3. 预写日志 (WAL) 文件
  4. 共享内存文件
  5. 报表日志
  6. 临时数据库
  7. 视图和子查询的具体化
  8. 瞬态指标
  9. VACUUM 使用的瞬态数据库

有关这些临时文件类型的其他信息在续集中。

2.1. 回滚日志

回滚日志是一个临时文件,用于在 SQLite 中实现原子提交和回滚功能。(有关其工作原理的详细讨论,请参阅名为 Atomic Commit In SQLite的单独文档。)回滚日志始终位于与数据库文件相同的目录中,并且与数据库文件具有相同的名称,除了 8 个字符“ -杂志” 附加。回滚日志通常在事务首次启动时创建,通常在事务提交或回滚时删除。回滚日志文件对于实现 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 日志模式的事务中发生崩溃或断电可能会导致数据库损坏。MEMORY 日志模式导致回滚日志存储在内存中而不是磁盘上。当日志模式为 MEMORY 时,ROLLBACK 命令仍然有效,但由于磁盘上不存在用于恢复的文件,因此在使用 MEMORY 日志模式的事务中发生崩溃或断电可能会导致数据库损坏。MEMORY 日志模式导致回滚日志存储在内存中而不是磁盘上。当日志模式为 MEMORY 时,ROLLBACK 命令仍然有效,但由于磁盘上不存在用于恢复的文件,因此在使用 MEMORY 日志模式的事务中发生崩溃或断电可能会导致数据库损坏。

2.2. 预写日志 (WAL) 文件

当 SQLite 在WAL 模式下 运行时,使用预写日志或 WAL 文件代替回滚日志与回滚日志一样,WAL 文件的目的是实现原子提交和回滚。WAL 文件始终位于与数据库文件相同的目录中,并且具有与数据库文件相同的名称,只是附加了 4 个字符“ -wal ”。WAL 文件在与数据库的第一个连接打开时创建,通常在与数据库的最后一个连接关闭时删除。但是,如果最后一个连接没有完全关闭,WAL 文件将保留在文件系统中,并在下次打开数据库时自动清理。

2.3. 共享内存文件

WAL 模式下运行时,与同一个数据库文件关联的所有 SQLite 数据库连接需要共享一些内存,用作 WAL 文件的索引。在大多数实现中,此共享内存是通过在为此唯一目的创建的文件(共享内存文件)上调用 mmap() 来实现的。共享内存文件(如果存在)位于与数据库文件相同的目录中,并且与数据库文件同名,只是附加了 4 个字符“ -shm ”。共享内存文件仅在 WAL 模式下运行时存在。

共享内存文件不包含持久内容。共享内存文件的唯一目的是提供一块共享内存供多个进程使用,所有进程都以 WAL 模式访问同一个数据库。如果VFS能够提供访问共享内存的替代方法,则可以使用该替代方法而不是共享内存文件。例如,如果PRAGMA locking_mode设置为 EXCLUSIVE(意味着只有一个进程能够访问数据库文件),那么共享内存将从堆中分配,而不是从共享内存文件中分配,并且共享内存文件将永远不会被创造。

共享内存文件与其关联的 WAL 文件具有相同的生命周期。共享内存文件在创建 WAL 文件时创建,并在删除 WAL 文件时删除。在 WAL 文件恢复过程中,共享内存文件会根据正在恢复的 WAL 文件的内容从头开始重新创建。

2.4. Super-Journal Files

当单个事务对已使用ATTACH语句添加到单个数据库连接的 多个数据库进行更改时,超级日志文件用作原子提交过程的一部分。超级日志文件始终位于与主数据库文件相同的目录中(主数据库文件是在创建数据库连接的原始sqlite3_open()sqlite3_open16()sqlite3_open_v2()调用中标识的数据库) 带有随机后缀。超级日志文件包含在事务期间更改的所有各种附加辅助数据库的名称。删除超级日志文件时,多库事务提交。有关更多详细信息,请参阅标题为 SQLite 中的原子提交的文档。

如果没有超级日志,多数据库事务上的事务提交对于每个数据库都是原子的,但它不会在所有数据库中都是原子的。换句话说,如果提交在中途因崩溃或断电而中断,那么对其中一个数据库的更改可能会完成,而对另一个数据库的更改可能会回滚。超级日志导致所有数据库中的所有更改一起回滚或提交。

超级日志文件仅为涉及多个数据库文件的COMMIT操作创建,其中至少两个数据库满足以下所有要求:

  1. 数据库被事务修改
  2. PRAGMA同步设置不是OFF
  3. PRAGMA journal_mode不是OFF、MEMORY 或 WAL

这意味着当数据库文件同步关闭或当它们使用 OFF、MEMORY 或 WAL 日志模式时,SQLite 事务在断电时不是跨多个数据库文件的原子。对于同步 OFF 和 journal_modes OFF 和 MEMORY,如果事务提交因断电而中断,数据库通常会损坏。对于 WAL 模式,单个数据库文件在断电时自动更新,但在多文件事务的情况下,一些文件可能会回滚,而其他文件可能会在电源恢复后前滚。

2.5. Statement Journal Files

语句日志文件用于回滚较大事务中单个语句的部分结果。例如,假设 UPDATE 语句将尝试修改数据库中的 100 行。但是在修改前 50 行之后,UPDATE 会遇到约束违规,这应该会阻止整个语句。语句日志用于撤消前 50 行更改,以便数据库恢复到语句开始时的状态。

仅为 UPDATE 或 INSERT 语句创建语句日志,这些语句可能会更改数据库的多行并且可能会在触发器中遇到约束或 RAISE 异常,因此需要撤消部分结果。如果 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 语句实现的更改大小成正比。

2.6. 临时数据库

使用“CREATE TEMP TABLE”语法创建的表仅对最初评估“CREATE TEMP TABLE”语句的数据库连接可见。这些 TEMP 表连同任何关联的索引、触发器和视图一起存储在一个单独的临时数据库文件中,该文件在看到第一个“CREATE TEMP TABLE”语句时立即创建。这个单独的临时数据库文件也有一个关联的回滚日志。当使用sqlite3_close()关闭数据库连接时,用于存储 TEMP 表的临时数据库文件会自动删除

TEMP 数据库文件与使用ATTACH语句 添加的辅助数据库文件非常相似,但具有一些特殊属性。TEMP 数据库总是在 数据库连接关闭时自动删除。TEMP 数据库始终使用 synchronous=OFFjournal_mode=PERSIST PRAGMA 设置。并且,TEMP 数据库不能与DETACH一起使用,另一个进程也不能ATTACH TEMP 数据库。

与 TEMP 数据库及其回滚日志关联的临时文件仅在应用程序使用“CREATE TEMP TABLE”语句时创建。

2.7. 视图和子查询的具体化

包含子查询的查询有时必须单独评估子查询并将结果存储在临时表中,然后使用临时表的内容来评估外部查询。我们称之为“具体化”子查询。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);

如果列 ex2.b 上存在索引, 最新版本的 SQLite(3.5.4 2007-12-14 版)及更高版本)将自动重写。

如果 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 子句等因素。何时可以和不能展平查询的规则非常复杂,超出了本文档的范围。

2.8. Transient Indices

SQLite 可以使用临时索引来实现 SQL 语言功能,例如:

  • ORDER BY 或 GROUP BY 子句
  • 聚合查询中的 DISTINCT 关键字
  • 由 UNION、EXCEPT 或 INTERSECT 连接的复合 SELECT 语句

每个临时索引都存储在它自己的临时文件中。临时索引的临时文件在使用它的语句结束时自动删除。

SQLite 努力使用预先存在的索引来实现 ORDER BY 子句。如果合适的索引已经存在,SQLite 将遍历索引而不是基础表来提取请求的信息,从而使行以所需的顺序出现。但是,如果 SQLite 找不到合适的索引,它将评估查询并将每一行存储在一个瞬态索引中,该索引的数据是行数据,其键是 ORDER BY 术语。查询被评估后,SQLite 返回并从头到尾遍历临时索引,以便按所需顺序输出行。

SQLite 通过按照 GROUP BY 术语建议的顺序对输出行进行排序来实现 GROUP BY。每个输出行都与前一行进行比较,看它是否开始一个新的“组”。按 GROUP BY 条款排序的方式与按 ORDER BY 条款排序的方式完全相同。如果可能,使用预先存在的索引,但如果没有合适的索引可用,则会创建临时索引。

聚合查询上的 DISTINCT 关键字是通过在临时文件中创建临时索引并将每个结果行存储在该索引中来实现的。在计算新的结果行时,会检查它们是否已经存在于临时索引中,如果存在,则丢弃新的结果行。

复合查询的UNION运算符是通过在临时文件中创建临时索引,并将左右子查询的结果存储在临时索引中,丢弃重复的来实现的。在对两个子查询都进行了评估之后,瞬态索引从头到尾遍历以生成最终输出。

复合查询的EXCEPT操作符是通过在一个临时文件中创建一个瞬态索引,将左子查询的结果存储在这个瞬态索引中,然后从瞬态索引中移除右子查询的结果,最后从头到尾遍历索引来实现的end 得到最终的输出。

复合查询的 INTERSECT 运算符是通过创建两个单独的临时索引来实现的,每个临时索引都在一个单独的临时文件中。左子查询和右子查询分别被评估为一个单独的瞬态索引。然后将两个索引一起走,并输出出现在两个索引中的条目。

请注意,用于复合查询的 UNION ALL 运算符本身不使用瞬态索引(当然 UNION ALL 的左右子查询可能会使用瞬态索引,具体取决于它们的组成方式。)

2.9. VACUUM使用的瞬态数据库

VACUUM命令 的工作原理是创建一个临时文件,然后将整个数据库重建到该临时文件中。然后将临时文件的内容复制回原始数据库文件并删除临时文件。

VACUUM命令 创建的临时文件仅在命令本身的持续时间内存在。临时文件的大小不会大于原始数据库。

3. SQLITE_TEMP_STORE 编译时参数和 Pragma

与事务控制相关的临时文件,即回滚日志、超级日志、预写日志 (WAL) 文件和共享内存文件,总是写入磁盘。但其他类型的临时文件可能只存储在内存中,从不写入磁盘。除了回滚、超级和语句日志之外的临时文件是写入磁盘还是仅存储在内存中取决于SQLITE_TEMP_STORE编译时参数、 temp_store pragma和临时文件的大小。

SQLITE_TEMP_STORE编译参数是一个#define,其值为 0 到 3 之间的整数,包括 0 和 3。SQLITE_TEMP_STORE编译时参数的含义 如下:

  1. 无论temp_store pragma 的设置如何,临时文件总是存储在磁盘上
  2. 临时文件默认存储在磁盘上,但这可以被temp_store pragma覆盖。
  3. 临时文件默认存储在内存中,但这可以被temp_store pragma覆盖。
  4. 无论temp_store pragma 的设置如何,临时文件总是存储在内存中

SQLITE_TEMP_STORE编译时参数 的默认值为1,这意味着将临时文件存储在磁盘上,但提供使用temp_store pragma覆盖行为的选项

temp_store pragma有一个 整数值,它也会影响存储临时文件的位置的决定。temp_store pragma 的值具有以下含义:

  1. 根据SQLITE_TEMP_STORE编译时参数 的决定,使用磁盘或内存存储临时文件。
  2. 如果SQLITE_TEMP_STORE编译时参数指定临时文件的内存存储,则覆盖该决定并改用磁盘存储。否则请遵循SQLITE_TEMP_STORE编译时参数的建议。
  3. 如果SQLITE_TEMP_STORE编译时参数指定临时文件的磁盘存储,则覆盖该决定并改用内存存储。否则请遵循SQLITE_TEMP_STORE编译时参数的建议。

temp_store pragma 的默认设置为0,这意味着遵循SQLITE_TEMP_STORE编译时参数的建议。

重申一下,SQLITE_TEMP_STORE编译时参数和 temp_store pragma只影响临时文件,而不是回滚日志和超级日志。无论SQLITE_TEMP_STORE编译时参数和 temp_store pragma的设置如何,回滚日志和超级日志总是写入磁盘

4.其他临时文件优化

SQLite 使用最近读取和写入的数据库页面的页面缓存。此页面缓存不仅用于主数据库文件,还用于存储在临时文件中的临时索引和表。如果 SQLite 需要使用临时索引或表,并且SQLITE_TEMP_STORE编译时参数和 temp_store pragma设置为在磁盘上存储临时表和索引,则信息最初仍存储在页面缓存中的内存中。临时文件不会打开,信息不会真正写入磁盘,直到页面缓存已满。

这意味着对于临时表和索引很小(小到足以放入页面缓存)的许多常见情况,不会创建临时文件,也不会发生磁盘 I/O。只有当临时数据变得太大而无法放入 RAM 时,信息才会溢出到磁盘。

每个临时表和索引都有自己的页面缓存,它可以存储由 SQLITE_DEFAULT_TEMP_CACHE_SIZE 编译时参数确定的最大数据库页面数。(默认值为 500 页。)页面缓存中数据库页面的最大数量对于每个临时表和索引都是相同的。该值不能在运行时或在每个表或每个索引的基础上更改。每个临时文件都有自己的私有页面缓存,具有自己的 SQLITE_DEFAULT_TEMP_CACHE_SIZE 页面限制。

5.临时文件存储位置

在其中创建临时文件的目录或文件夹由特定于操作系统的VFS确定。

在类 unix 系统上,目录按以下顺序搜索:

  1. PRAGMA temp_store_directorysqlite3_temp_directory全局变量 设置的目录
  2. SQLITE_TMPDIR 环境变量
  3. TMPDIR 环境变量
  4. /变量/tmp
  5. /usr/tmp
  6. /tmp
  7. 当前工作目录 (".")
使用上述第一个被发现存在并设置了写入和执行位的。最后的“.” 对于一些在没有标准临时文件位置的 chroot jails 中使用 SQLite 的应用程序来说,fallback 很重要。

在 Windows 系统上,按以下顺序搜索文件夹:

  1. 由PRAGMA temp_store_directorysqlite3_temp_directory全局变量 设置的文件夹
  2. GetTempPath() 系统接口返回的文件夹。
在这种情况下,SQLite 本身并不关注环境变量,尽管 GetTempPath() 系统调用可能会关注。CYGWIN 构建的搜索算法不同。检查源代码以获取详细信息。