小巧、快速、可靠。
三选二。
RBU 扩展

1. RBU 扩展

RBU 扩展是 SQLite 的一个附加组件,专为在网络边缘的低功耗设备上使用大型 SQLite 数据库文件而设计。RBU 可用于两种独立的任务

首字母缩略词 RBU 代表“可恢复的批量更新”。

两种 RBU 功能都可以使用 SQLite 内置的 SQL 命令来完成 - RBU 更新通过一系列 INSERTDELETEUPDATE 命令在一个事务中完成,RBU vacuum 通过一个 VACUUM 命令完成。与这些更简单的方法相比,RBU 模块提供了以下优势

  1. RBU 可能更高效

    将更改应用于 B 树(SQLite 用于在磁盘上存储每个表和索引的数据结构)的最有效方法是按键顺序进行更改。但是,如果 SQL 表有一个或多个索引,则每个索引的键顺序可能与主表和其他辅助索引不同。因此,在执行一系列 INSERTUPDATEDELETE 语句时,通常无法对操作进行排序,以使所有 B 树都按键顺序更新。RBU 更新过程通过在一遍中对主表应用所有更改,然后在单独的遍中对每个索引应用更改来解决此问题,从而确保每个 B 树都得到最佳更新。对于大型数据库文件(不适合 OS 磁盘缓存的文件),此过程可以使更新速度提高两个数量级。

    RBU Vacuum 操作所需的临时磁盘空间更少,并且写入磁盘的数据也更少,比 SQLite VACUUM 更少。SQLite VACUUM 需要大约最终数据库文件大小的两倍的临时磁盘空间才能运行。写入的数据总量大约是最终数据库文件大小的三倍。相比之下,RBU Vacuum 需要大约最终数据库文件大小的临时磁盘空间,并且总共写入磁盘的磁盘空间是其两倍。

    另一方面,RBU Vacuum 比常规 SQLite VACUUM 使用更多的 CPU - 在一项测试中,其 CPU 使用量多达五倍。因此,在相同条件下,RBU Vacuum 通常比 SQLite VACUUM 慢得多。

  2. RBU 在后台运行

    正在进行的 RBU 操作(更新或真空)不会干扰对数据库文件的读取访问。

  3. RBU 递增运行

    RBU 操作可以暂停,然后在以后恢复,可能会有中间的电源故障和/或系统重置。对于 RBU 更新,原始数据库内容对所有数据库读取器保持可见,直到整个更新应用完毕 - 即使更新被暂停并在以后恢复。

RBU 扩展默认情况下未启用。要启用它,请使用 SQLITE_ENABLE_RBU 编译时选项编译 amalgamation

2. RBU 更新

2.1. RBU 更新限制

以下限制适用于 RBU 更新

2.2. 准备 RBU 更新文件

RBU 要应用的所有更改都存储在一个名为“RBU 数据库”的单独 SQLite 数据库中。要修改的数据库称为“目标数据库”。

对于目标数据库中每个将通过更新修改的表,都会在 RBU 数据库中创建一个相应的表。RBU 数据库表的模式与目标数据库的模式不同,而是根据 以下说明派生出来的。

RBU 数据库表包含一个针对目标数据库每个由更新插入、更新或删除的行。在 下一节中描述了填充 RBU 数据库表的方法。

2.2.1. RBU 数据库模式

对于目标数据库中的每个表,RBU 数据库都应该包含一个名为“data<integer>_<target-table-name>”的表,其中 <target-table-name> 是目标数据库中表的名称,<integer> 是任何零个或多个数字字符 (0-9) 的序列。RBU 数据库中的表按名称顺序处理(从 BINARY 排序顺序中最小的到最大的),因此目标表更新的顺序受 data_% 表名称中 <integer> 部分的选择的影响。虽然这在使用 RBU 更新 某些类型的虚拟表时可能有用,但通常没有理由在 <integer> 的位置使用除空字符串以外的任何内容。

data_% 表必须具有与目标表完全相同的列,以及一个名为“rbu_control”的附加列。data_% 表不应具有 PRIMARY KEY 或 UNIQUE 约束,但每列应具有与目标数据库中对应列相同的类型。rbu_control 列不应有任何类型。例如,如果目标数据库包含

CREATE TABLE t1(a INTEGER PRIMARY KEY, b TEXT, c UNIQUE);

则 RBU 数据库应该包含

CREATE TABLE data_t1(a INTEGER, b TEXT, c, rbu_control);

data_% 表中列的顺序无关紧要。

如果目标数据库表是虚拟表或没有 PRIMARY KEY 声明的表,则 data_% 表还必须包含一个名为“rbu_rowid”的列。rbu_rowid 列映射到表的 ROWID。例如,如果目标数据库包含以下两种情况之一

CREATE VIRTUAL TABLE x1 USING fts3(a, b);
CREATE TABLE x1(a, b);

则 RBU 数据库应该包含

CREATE TABLE data_x1(a, b, rbu_rowid, rbu_control);

对于“rowid”列不像主键值一样工作的虚拟表,不能使用 RBU 更新。

目标表的所有非隐藏列(即与“SELECT *”匹配的所有列)都必须出现在输入表中。对于虚拟表,隐藏列是可选的 - 如果输入表中存在,则由 RBU 更新,否则不更新。例如,要写入具有隐藏 languageid 列的 fts4 表,例如

CREATE VIRTUAL TABLE ft1 USING fts4(a, b, languageid='langid');

以下任何一个输入表模式都可以使用

CREATE TABLE data_ft1(a, b, langid, rbu_rowid, rbu_control);
CREATE TABLE data_ft1(a, b, rbu_rowid, rbu_control);

2.2.2. RBU 数据库内容

对于作为 RBU 更新的一部分要插入目标数据库的每一行,相应的 data_% 表都应该包含一个记录,其中“rbu_control”列设置为包含整数 0。其他列应设置为构成要插入的新记录的值。

“rbu_control”列还可以设置为整数 2 以进行 INSERT。在这种情况下,新行会静默地替换任何具有相同主键值的现有行。这等效于 DELETE 后跟具有相同主键值的 INSERT。它与 SQL REPLACE 命令不同,因为在这种情况下,新行可能会替换任何冲突的行(即由于 UNIQUE 约束或索引而冲突的行),而不仅仅是具有冲突主键的行。

如果目标数据库表具有 INTEGER PRIMARY KEY,则无法将 NULL 值插入 IPK 列。尝试这样做会导致 SQLITE_MISMATCH 错误。

对于作为 RBU 更新的一部分要从目标数据库中删除的每一行,相应的 data_% 表都应该包含一个记录,其中“rbu_control”列设置为包含整数 1。要删除的行实际主键值应存储在 data_% 表的对应列中。其他列中存储的值未使用。

对于作为 RBU 更新的一部分要从目标数据库中更新的每一行,相应的 data_% 表都应该包含一个记录,其中“rbu_control”列设置为包含文本类型的值。要更新行的实际主键值应存储在 data_% 表行的对应列中,以及所有要更新的列的新值。rbu_control 列中的文本值必须包含与目标数据库表中的列数相同的字符数,并且必须完全由 'x' 和 '.' 字符组成(或在某些特殊情况下为 'd' - 参见下文)。对于要更新的每列,相应的字符设置为 'x'。对于保持不变的列,rbu_control 值的相应字符应设置为 '.'。例如,给定上面的表,更新语句

UPDATE t1 SET c = 'usa' WHERE a = 4;

由创建的 data_t1 行表示

INSERT INTO data_t1(a, b, c, rbu_control) VALUES(4, NULL, 'usa', '..x');

如果 RBU 用于更新目标数据库中的大型 BLOB 值,则将存储可用于修改现有 BLOB 的补丁或增量,而不是在 RBU 数据库中存储完全新的值,这可能更高效。RBU 允许以两种方式指定增量

化石增量格式只能用于更新 BLOB 值。它不会将新的 BLOB 存储在 data_% 表中,而是存储化石增量。同样,更新列的 rbu_control 字符串不再使用 'x',而是使用 'f' 字符。在处理 'f' 更新时,RBU 会从磁盘加载原始 BLOB 数据,应用化石增量,并将结果存储回数据库文件。由 sqldiff --rbu 生成的 RBU 数据库在可能节省 RBU 数据库空间的情况下,都会使用化石增量。

要使用自定义增量格式,RBU 应用程序必须在开始处理更新之前,注册一个名为 "rbu_delta" 的用户定义的 SQL 函数。rbu_delta() 函数会使用两个参数调用:目标表列中存储的原始值和 RBU 更新提供的增量值。它应该返回将增量应用于原始值的计算结果。要使用自定义增量函数,与要更新的目标列对应的 rbu_control 值的字符必须设置为 'd',而不是 'x'。然后,RBU 会调用用户定义的 SQL 函数 "rbu_delta()",而不是用 data_% 列中存储的值更新目标表,并将结果存储在目标表列中。

例如,这行

INSERT INTO data_t1(a, b, c, rbu_control) VALUES(4, NULL, 'usa', '..d');

导致 RBU 更新目标数据库表,类似于

UPDATE t1 SET c = rbu_delta(c, 'usa') WHERE a = 4;

如果目标数据库表是虚拟表或没有主键的表,则 rbu_control 值不应包含与 rbu_rowid 值对应的字符。例如,这

INSERT INTO data_ft1(a, b, rbu_rowid, rbu_control) 
  VALUES(NULL, 'usa', 12, '.x');

导致类似于以下的结果

UPDATE ft1 SET b = 'usa' WHERE rowid = 12;

data_% 表本身不应包含主键声明。但是,如果以 "rowid" 顺序读取每个 data_% 表中的行,与按对应目标数据库表的 PRIMARY KEY 排序读取行大致相同,则 RBU 的效率会更高。换句话说,在将行插入 data_% 表之前,应该使用目标表的 PRIMARY KEY 字段对行进行排序。

2.2.3. 将 RBU 与 FTS3/4 表一起使用

通常,FTS3 或 FTS4 表是虚拟表的示例,其 rowid 功能类似于主键。因此,对于以下 FTS4 表

CREATE VIRTUAL TABLE ft1 USING fts4(addr, text);
CREATE VIRTUAL TABLE ft2 USING fts4;             -- implicit "content" column

可以按以下方式创建 data_% 表

CREATE TABLE data_ft1 USING fts4(addr, text, rbu_rowid, rbu_control);
CREATE TABLE data_ft2 USING fts4(content, rbu_rowid, rbu_control);

并按目标表是普通 SQLite 表且没有显式主键列的方式进行填充。

无内容 FTS4 表 的处理方式类似,只是在应用更新时,任何尝试更新或删除行的操作都会导致错误。

外部内容 FTS4 表 也可以使用 RBU 进行更新。在这种情况下,用户需要配置 RBU 数据库,以便对 FTS4 索引应用与对基础内容表相同的 UPDATE、DELETE 和 INSERT 操作。对于所有外部内容 FTS4 表的更新,用户还需要确保在将 UPDATE 或 DELETE 操作应用于基础内容表之前,将其应用于 FTS4 索引(有关详细说明,请参阅 FTS4 文档)。在 RBU 中,这是通过确保用于写入 FTS4 表的 data_% 表的名称在使用 BINARY 排序规则更新基础内容表的 data_% 表的名称之前进行排序来完成的。为了避免在 RBU 数据库中重复数据,可以使用 SQL 视图代替其中一个 data_% 表。例如,对于目标数据库模式

CREATE TABLE ccc(addr, text);
CREATE VIRTUAL TABLE ccc_fts USING fts4(addr, text, content=ccc);

可以使用以下 RBU 数据库模式

CREATE TABLE data_ccc(addr, text, rbu_rowid, rbu_control);
CREATE VIEW data0_ccc_fts AS SELECT * FROM data_ccc;

然后可以按正常方式填充 data_ccc 表,其中包含要更新目标数据库表 ccc 的更新。RBU 会从 data0_ccc_fts 视图读取相同的更新,并将其应用于 FTS 表 ccc_fts。因为 "data0_ccc_fts" 比 "data_ccc" 小,所以 FTS 表会首先更新,这是必需的。

基础内容表具有显式 INTEGER 主键列的情况稍微复杂一些,因为存储在 rbu_control 列中的文本值对于 FTS 索引及其基础内容表略有不同。对于基础内容表,必须在任何用于显式 IPK 的 rbu_control 文本值中包含一个字符,但对于具有隐式 rowid 的 FTS 表本身,则不应包含。这很不方便,但可以使用更复杂的视图来解决,如下所示

-- Target database schema
CREATE TABLE ddd(i INTEGER PRIMARY KEY, k TEXT);
CREATE VIRTUAL TABLE ddd_fts USING fts4(k, content=ddd);

-- RBU database schema
CREATE TABLE data_ccc(i, k, rbu_control);
CREATE VIEW data0_ccc_fts AS SELECT i AS rbu_rowid, k, CASE 
  WHEN rbu_control IN (0,1) THEN rbu_control ELSE substr(rbu_control, 2) END
FROM data_ccc;

上面 SQL 视图中的 substr() 函数返回 rbu_control 参数的文本,其中第一个字符(与列 "i" 对应的字符,FTS 表不需要)已删除。

2.2.4. 使用 sqldiff 自动生成 RBU 更新

从 SQLite 版本 3.9.0(2015-10-14)开始,sqldiff 实用程序能够生成 RBU 数据库,表示两个具有相同模式的数据库之间的差异。例如,以下命令

sqldiff --rbu t1.db t2.db

输出一个 SQL 脚本,用于创建一个 RBU 数据库,如果用于更新数据库 t1.db,则会将其修补,使其内容与数据库 t2.db 的内容相同。

默认情况下,sqldiff 会尝试处理两个提供给它的数据库中的所有非虚拟表。如果任何表出现在一个数据库中但未出现在另一个数据库中,或者如果任何表在一个数据库中的模式略有不同,则会发生错误。如果出现此问题,"--table" 选项可能很有用

默认情况下,sqldiff 会忽略虚拟表。但是,可以使用类似以下的命令显式地为具有充当主键功能的 rowid 的虚拟表创建 RBU data_% 表

sqldiff --rbu --table <virtual-table-name> t1.db t2.db

不幸的是,即使默认情况下会忽略虚拟表,但它们为在数据库中存储数据而创建的任何 基础数据库表 不会被忽略,并且 sqldiff 会将这些表包含在任何 RBU 数据库中。因此,尝试使用 sqldiff 创建要应用于具有一个或多个虚拟表的目标数据库的 RBU 更新的用户,可能需要分别使用 --table 选项运行 sqldiff,以更新目标数据库中的每个表。

2.3. RBU 更新 C/C++ 编程

RBU 扩展接口允许应用程序将存储在 RBU 数据库中的 RBU 更新应用于现有目标数据库。过程如下

  1. 使用 sqlite3rbu_open(T,A,S) 函数打开 RBU 句柄。

    T 参数是目标数据库文件的名称。A 参数是 RBU 数据库文件的名称。S 参数是用于存储恢复更新(在中断后)所需状态信息的 "状态数据库" 的名称。S 参数可以为 NULL,在这种情况下,状态信息存储在 RBU 数据库中,这些信息存储在名称都以 "rbu_" 开头的各种表中。

    sqlite3rbu_open(T,A,S) 函数返回指向 "sqlite3rbu" 对象的指针,然后将其传递给后续接口。

  2. 使用由 sqlite3rbu_db(X) 返回的数据库句柄(其中参数 X 是从 sqlite3rbu_open() 返回的 sqlite3rbu 指针)注册任何必需的虚拟表模块。另外,如果需要,使用 sqlite3_create_function_v2() 注册 rbu_delta() SQL 函数。

  3. 在 sqlite3rbu 对象指针 X 上调用 sqlite3rbu_step(X) 函数一次或多次。每次调用 sqlite3rbu_step() 都执行一个 b 树操作,因此可能需要数千次调用才能应用完整的更新。当更新完全应用时,sqlite3rbu_step() 接口将返回 SQLITE_DONE。

  4. 调用 sqlite3rbu_close(X) 来销毁 sqlite3rbu 对象指针。如果已对 sqlite3rbu_step(X) 进行足够多次调用以将更新完全应用于目标数据库,则 RBU 数据库将标记为已完全应用。否则,RBU 更新应用程序的状态将保存在状态数据库中(如果 sqlite3rbu_open() 中的状态数据库文件名称为 NULL,则保存在 RBU 数据库中),以便以后恢复更新。

如果在调用 sqlite3rbu_close() 时,更新仅部分应用于目标数据库,则状态信息将保存在状态数据库中(如果存在),否则保存在 RBU 数据库中。这允许后续进程自动从停止的地方恢复 RBU 更新。如果状态信息存储在 RBU 数据库中,则可以通过删除名称以 "rbu_" 开头的所有表来将其删除。

有关更多详细信息,请参阅 头文件 sqlite3rbu.h 中的注释。

3. RBU Vacuum

3.1. RBU Vacuum 的限制

与 SQLite 的内置 VACUUM 命令相比,RBU Vacuum 有以下限制

3.2. RBU Vacuum C/C++ 编程

本节概述了将 RBU Vacuum 集成到应用程序程序中的过程,并提供了示例代码进行演示。有关完整详细信息,请参阅 头文件 sqlite3rbu.h 中的注释。

所有 RBU Vacuum 应用程序都实现以下过程的某种变体

  1. 通过调用 sqlite3rbu_vacuum(T, S) 创建 RBU 句柄。

    T 参数是要清理的数据库文件的名称。S 参数是 RBU 模块将在其中保存其状态的数据库的名称,如果真空操作被挂起。

    如果在调用 sqlite3rbu_vacuum() 时,状态数据库 S 不存在,则会自动创建该数据库,并使用用于存储 RBU 真空状态的单个表 "rbu_state" 进行填充。如果正在进行的 RBU 真空被挂起,则此表会填充状态数据。下次调用 sqlite3rbu_vacuum() 使用相同的 S 参数时,它会检测到此数据,并尝试恢复挂起的真空操作。当 RBU 真空操作完成或遇到错误时,RBU 会自动删除 rbu_state 表的内容。在这种情况下,下次调用 sqlite3rbu_vacuum() 会从头开始一个全新的真空操作。

    最好建立一个约定,根据目标数据库名称确定 RBU 真空状态数据库名称。下面的示例代码使用 "<target>-vacuum",其中 <target> 是正在清理的数据库的名称。

  2. 正在清理的数据库中索引使用的任何自定义排序规则都会在由 sqlite3rbu_db() 函数返回的两个数据库句柄中注册。

  3. 在 RBU 句柄上调用函数 sqlite3rbu_step(),直到 RBU 真空完成、发生错误或应用程序希望挂起 RBU 真空。

    每次调用 sqlite3rbu_step() 都会对完成真空操作进行少量工作。根据数据库的大小,单个真空可能需要数千次调用 sqlite3rbu_step()。如果真空操作已完成,sqlite3rbu_step() 会返回 SQLITE_DONE,如果真空操作尚未完成但未发生错误,则会返回 SQLITE_OK,如果遇到错误,则会返回 SQLite 错误代码。如果确实发生错误,则所有后续对 sqlite3rbu_step() 的调用会立即返回相同的错误代码。

  4. 最后,调用 sqlite3rbu_close() 来关闭 RBU 句柄。如果应用程序在真空完成或发生错误之前停止调用 sqlite3rbu_step(),则真空的状态将保存在状态数据库中,以便以后恢复。

    与 sqlite3rbu_step() 一样,如果 vacuum 操作已完成,sqlite3rbu_close() 返回 SQLITE_DONE。如果 vacuum 尚未完成但没有发生错误,则返回 SQLITE_OK。或者,如果发生错误,则返回一个 SQLite 错误代码。如果在之前调用 sqlite3rbu_step() 时发生了错误,则 sqlite3rbu_close() 返回相同的错误代码。

以下示例代码演示了上述技术。

/*
** Either start a new RBU vacuum or resume a suspended RBU vacuum on 
** database zTarget. Return when either an error occurs, the RBU 
** vacuum is finished or when the application signals an interrupt
** (code not shown).
**
** If the RBU vacuum is completed successfully, return SQLITE_DONE.
** If an error occurs, return SQLite error code. Or, if the application
** signals an interrupt, suspend the RBU vacuum operation so that it
** may be resumed by a subsequent call to this function and return
** SQLITE_OK.
**
** This function uses the database named "<zTarget>-vacuum" for
** the state database, where <zTarget> is the name of the database 
** being vacuumed.
*/
int do_rbu_vacuum(const char *zTarget){
  int rc;
  char *zState;                   /* Name of state database */
  sqlite3rbu *pRbu;               /* RBU vacuum handle */

  zState = sqlite3_mprintf("%s-vacuum", zTarget);
  if( zState==0 ) return SQLITE_NOMEM;
  pRbu = sqlite3rbu_vacuum(zTarget, zState);
  sqlite3_free(zState);

  if( pRbu ){
    sqlite3 *dbTarget = sqlite3rbu_db(pRbu, 0);
    sqlite3 *dbState = sqlite3rbu_db(pRbu, 1);

    /* Any custom collation sequences used by the target database must
    ** be registered with both database handles here.  */

    while( sqlite3rbu_step(pRbu)==SQLITE_OK ){
      if( <application has signaled interrupt> ) break;
    }
  }
  rc = sqlite3rbu_close(pRbu);
  return rc;
}

此页面最后修改于 2022-01-08 05:02:57 UTC