RBU 扩展

1. RBU 扩展

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

  • RBU Update operations. An RBU Update is a bulk update of a database file that may include many insert, update and delete operations on one or more tables.
  • RBU Vacuum operations. An RBU Vacuum optimizes and rebuilds an entire database file, with results similar to SQLite's native VACUUM command.

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

这两个 RBU 功能都可以使用 SQLite 的内置 SQL 命令来完成——RBU 通过单个事务中 的一系列INSERTDELETEUPDATE命令更新,以及通过单个VACUUM命令进行 RBU 清理。与这些更简单的方法相比,RBU 模块具有以下优势:

  1. RBU 可能更有效率

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

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

    另一方面,RBU Vacuum 使用的 CPU 比常规 SQLite VACUUM 多——在一次测试中是它的五倍。出于这个原因,在相同条件下,RBU Vacuum 通常比 SQLite VACUUM 慢得多。

  2. RBU 在后台运行

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

  3. RBU 增量运行

    RBU 操作可能会暂停,然后在稍后恢复,可能会出现断电和/或系统重置。对于 RBU 更新,原始数据库内容对所有数据库读取器保持可见,直到应用了整个更新 - 即使更新被暂停然后稍后恢复。

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

2. RBU更新

2.1. RBU 更新限制

以下限制适用于 RBU 更新:

  • 更改必须仅包含INSERTUPDATEDELETE 操作。不支持 CREATE 和 DROP 操作。

  • INSERT语句不能使用默认值。

  • UPDATEDELETE语句必须通过 rowid 或非 NULL PRIMARY KEY 值来标识目标行。

  • UPDATE语句不能修改 PRIMARY KEY 或 rowid 值。

  • RBU 更新不能应用于任何包含名为“rbu_control”的列的表。

  • RBU 更新不会触发任何触发器。

  • RBU 更新不会检测或防止违反外键或 CHECK 约束。

  • 所有 RBU 更新都使用“OR ROLLBACK”约束处理机制。

  • 目标数据库可能未处于WAL 模式

  • 目标数据库可能不包含表达式的索引 从 SQLite 3.30.0 (2019-10-04) 开始支持表达式索引。
  • 应用 RBU 更新时,目标数据库上不会发生其他写入。在目标数据库上持有读锁以防止这种情况发生。

2.2. 准备 RBU 更新文件

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

对于目标数据库中将被更新修改的每个表,都会在 RBU 数据库中创建一个对应的表。RBU 数据库表模式与目标数据库的不同,但如下所述从中派生。

RBU 数据库表包含由更新插入、更新或删除的每个目标数据库行的单个行。下一节将介绍填充 RBU 数据库表

2.2.1. RBU 数据库模式

对于目标数据库中的每个表,RBU 数据库应包含一个名为“data< integer >_< target-table-name >”的表,其中 < target-table-name > 是目标数据库中表的名称,<整数> 是零个或多个数字字符 (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。其他列应设置为使向上插入新记录。

对于 INSERT,“rbu_control”列也可以设置为整数值 2。在这种情况下,新行会自动替换任何具有相同主键值的现有行。这等效于 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 值,则在 RBU 数据库中存储可用于修改现有 BLOB 的补丁或增量而不是全新的值可能更有效。RBU 允许以两种方式指定增量:

fossil delta 格式只能用于更新 BLOB 值。不是将新的 BLOB 存储在 data_% 表中,而是存储化石增量。并且没有将“x”指定为要更新的列的 rbu_control 字符串的一部分,而是存储了“f”字符。处理“f”更新时,RBU 从磁盘加载原始 BLOB 数据,对其应用化石增量并将结果存储回数据库文件。sqldiff --rbu生成的 RBU 数据库使用化石增量,只要这样做可以节省 RBU 数据库中的空间。

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

例如,这一行:

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;

如果目标数据库表是虚拟表或没有PRIMARY KEY的表,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_% 表本身应该没有 PRIMARY KEY 声明。但是,如果以“rowid”顺序从每个 data_% 表中读取行与读取按相应目标数据库表的 PRIMARY KEY 排序的行大致相同,则 RBU 效率更高。换句话说,在将行插入 data_% 表之前,应使用目标表的 PRIMARY KEY 字段对行进行排序。

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

通常,FTS3 或 FTS4表是具有 rowid 的虚拟表的示例,其工作方式类似于 PRIMARY KEY。因此,对于以下 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);

并像目标表是没有显式 PRIMARY KEY 列的普通 SQLite 表一样填充。

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

也可以使用 RBU 更新外部内容 FTS4 表。在这种情况下,用户需要配置 RBU 数据库,以便将相同的一组更新、删除和插入操作应用于 FTS4 索引作为基础内容表。对于外部内容 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 PRIMARY KEY 列的情况稍微困难一些,因为存储在 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 默认忽略虚拟表。但是,可以使用以下命令为虚拟表显式创建 RBU data_% 表,该表具有功能类似于主键的 rowid:

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真空

3.1. RBU 真空限制

与 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 vacuum 被挂起,则该表将填充状态数据。下次使用相同的 S 参数调用 sqlite3rbu_vacuum() 时,它会检测到此数据并尝试恢复挂起的真空操作。当 RBU vacuum 操作完成或遇到错误时,RBU 会自动删除 rbu_state 表的内容。在这种情况下,下一次调用 sqlite3rbu_vacuum() 从头开始​​一个全新的真空操作。

    根据目标数据库名称确定 RBU 真空状态数据库名称的约定是一个好主意。下面的示例代码使用“<target>-vacuum”,其中 <target> 是被清理的数据库的名称。

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

  3. 在 RBU 句柄上调用函数 sqlite3rbu_step() 直到 RBU 清理完成、发生错误或应用程序希望暂停 RBU 清理。

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

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

    和 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;
}