RBU扩展是SQLite的附加组件,旨在与网络边缘低功耗设备上的大型SQLite数据库文件一起使用。RBU可以用于两个单独的任务:
首字母缩写词RBU代表“可恢复的批量更新”。
这两个RBU功能都可以使用SQLite的内置SQL命令来完成-通过单个事务中的一系列INSERT,DELETE和 UPDATE命令进行RBU更新,以及通过单个VACUUM命令进行RBU清理 。与这些较简单的方法相比,RBU模块具有以下优点:
将更改应用于B树(SQLite用于在磁盘上存储每个表和索引的数据结构)的最有效方法是按键顺序进行更改。但是,如果SQL表具有一个或多个索引,则每个索引的键顺序可能与主表和其他辅助索引不同。结果,当执行一系列INSERT, UPDATE和DELETE时语句通常无法对操作进行排序,以使所有b树都按键顺序进行更新。RBU更新过程通过以下方法解决此问题:一次将所有更改应用于主表,然后在单独的通道中将更改应用于每个索引,以确保最佳地更新每个B-Tree。对于大型数据库文件(一个不适合OS磁盘缓存的文件),此过程可以使更新速度提高两个数量级。
与SQLite VACUUM相比,RBU Vacuum操作所需的临时磁盘空间更少,并且向磁盘写入的数据更少。SQLite VACUUM需要大约临时磁盘空间中最终数据库文件大小的两倍才能运行。写入的数据总量约为最终数据库文件大小的三倍。相比之下,RBU Vacuum需要临时磁盘空间中最终数据库文件的大小,并将总数写入磁盘的两倍。
另一方面,与常规SQLite VACUUM相比,RBU Vacuum使用的CPU多-在一项测试中,其消耗是其五倍。因此,在相同条件下,RBU真空通常比SQLite VACUUM慢得多。
正在进行的RBU操作(更新或清除)不会干扰对数据库文件的读取访问。
RBU的操作可能会暂停,然后再恢复,可能会发生中间停电和/或系统重置的情况。对于RBU更新,原始数据库内容对于所有数据库阅读器均保持可见,直到应用了整个更新-即使该更新被挂起,然后又恢复。
默认情况下,RBU扩展未启用。要启用它,请使用SQLITE_ENABLE_RBU编译时选项编译 合并。
以下限制适用于RBU更新:
INSERT语句可能不使用默认值。
UPDATE语句不能修改PRIMARY KEY或rowid值。
RBU更新不能应用于包含名为“ rbu_control”的列的任何表。
RBU更新不会触发任何触发器。
RBU更新将不会检测或阻止外键或CHECK约束冲突。
所有RBU更新都使用“ OR ROLLBACK”约束处理机制。
目标数据库可能未处于WAL模式。
应用RBU更新时,在目标数据库上可能不会发生其他写入。在目标数据库上会保留一个读锁,以防止发生这种情况。
RBU将要应用的所有更改都存储在一个单独的SQLite数据库中,该数据库称为“ RBU数据库”。要修改的数据库称为“目标数据库”。
对于目标数据库中将通过更新进行修改的每个表,都会在RBU数据库中创建一个对应的表。RBU数据库表架构与目标数据库的架构不同,但如下所述是从其派生的。
RBU数据库表为更新插入,更新或删除的每个目标数据库行包含一行。下一节将介绍如何填充RBU数据库表。
对于目标数据库中的每个表,RBU数据库应包含一个名为“ data <整数> _ <目标表名称>”的表,其中<目标表名称>是目标数据库中表的名称,而<整数>是零个或多个数字字符(0-9)的任何序列。RBU数据库中的表按名称顺序(根据BINARY排序顺序从最小到最大)进行处理,因此,目标表的更新顺序受data_%表名的< integer >部分的选择影响。。尽管在使用RBU更新某些类型的虚拟表时这很有用 ,通常没有理由使用空字符串代替< integer >。
data_%表必须具有与目标表相同的所有列,外加一列名为“ rbu_control”的列。data_%表不应具有PRIMARY KEY或UNIQUE约束,但每一列应与目标数据库中的对应列具有相同的类型。rbu_control列应该根本没有类型。例如,如果目标数据库包含:
创建表t1(a整数主键,b文本,c唯一);
然后,RBU数据库应包含:
创建表data_t1(a INTEGER,b TEXT,c,rbu_control);
data_%表中列的顺序无关紧要。
如果目标数据库表是虚拟表或没有PRIMARY KEY声明的表,则data_%表还必须包含一个名为“ rbu_rowid”的列。rbu_rowid列映射到表ROWID。例如,如果目标数据库包含以下任一:
使用fts3(a,b)创建虚拟表x1; 创建表x1(a,b);
那么RBU数据库应包含:
创建表data_x1(a,b,rbu_rowid,rbu_control);
无法使用RBU更新“ rowid”列不能像主键值那样起作用的虚拟表。
目标表的所有非隐藏列(即,所有与“ SELECT *”匹配的列)都必须存在于输入表中。对于虚拟表,隐藏列是可选的-如果输入表中存在隐藏列,则它们由RBU更新,否则不更新。例如,要使用隐藏的languageid列写入fts4表,例如:
使用fts4(a,b,languageid ='langid')创建虚拟表ft1;
可以使用以下两种输入表模式之一:
创建表data_ft1(a,b,langid,rbu_rowid,rbu_control); 创建表data_ft1(a,b,rbu_rowid,rbu_control);
对于作为RBU更新的一部分插入到目标数据库中的每一行,对应的data_%表应包含一条记录,其中“ rbu_control”列设置为包含整数值0。其他列应设置为插入新记录。
对于INSERT,“ rbu_control”列也可以设置为整数2。在这种情况下,新行将无提示替换任何具有相同主键值的现有行。这等效于后跟具有相同主键值的INSERT的DELETE。它与SQL REPLACE命令不同,因为在这种情况下,新行可以替换任何冲突的行(即由于UNIQUE约束或索引而冲突的行),而不仅仅是具有主键冲突的行。
如果目标数据库表具有INTEGER PRIMARY KEY,则不可能在IPK列中插入NULL值。尝试这样做会导致SQLITE_MISMATCH错误。
对于作为RBU更新的一部分从目标数据库中删除的每一行,相应的data_%表应包含一条记录,其中“ rbu_control”列设置为包含整数值1。要删除的行的实际主键值应存储在data_%表的相应列中。不使用其他列中存储的值。
对于作为RBU更新的一部分从目标数据库进行UPDATE的每一行,相应的data_%表应包含一条记录,其中“ rbu_control”列设置为包含text类型的值。标识要更新的行的实际主键值应存储在data_%表行的相应列中,所有列的新值也应更新。“ rbu_control”列中的文本值必须包含与目标数据库表中的列相同数量的字符,并且必须完全由“ x”和“'”组成。字符(或在某些特殊情况下为'd'-参见下文)。对于每个要更新的列,相应的字符都设置为“ x”。对于那些保持原样的人,rbu_control值的相应字符应设置为“。”。例如,给定以上表,update语句:
更新t1设置c ='美国'其中a = 4;
由以下创建的data_t1行表示:
插入到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”。然后,不是用存储在相应data_%列中的值更新目标表,而是RBU调用用户定义的SQL函数“ rbu_delta()”和目标表列中的存储。
例如,此行:
插入到data_t1(a,b,c,rbu_control)VALUES(4,NULL,'usa','..d');
使RBU以类似于以下的方式更新目标数据库表:
更新t1设置c = rbu_delta(c,'美国')其中a = 4;
如果目标数据库表是虚拟表或没有PRIMARY KEY的表,则rbu_control值不应包含与rbu_rowid值相对应的字符。例如,这:
插入到data_ft1(a,b,rbu_rowid,rbu_control) VALUES(NULL,'usa',12,'.x');
导致类似于以下结果:
更新ft1设置b ='美国',rowid = 12;
data_%表本身不应具有PRIMARY KEY声明。但是,如果按“行”顺序从每个data_%表中读取行与按相应目标数据库表的PRIMARY KEY排序读取行大致相同,则RBU效率更高。换句话说,在将行插入到data_%表之前,应使用目标表的PRIMARY KEY字段对行进行排序。
通常,FTS3或FTS4表是虚拟表的示例,其虚拟行的行为类似于PRIMARY KEY。因此,对于以下FTS4表:
使用fts4(addr,text)创建虚拟表ft1; 使用fts4创建虚拟表ft2;-隐式的“内容”列
可以按以下方式创建data_%表:
使用fts4(addr,text,rbu_rowid,rbu_control)创建表data_ft1; 使用fts4(content,rbu_rowid,rbu_control)创建表data_ft2;
并将其填充为目标表是没有显式PRIMARY KEY列的普通SQLite表。
无内容的FTS4表的处理方式类似,不同之处在于,任何尝试更新或删除行的尝试都会在应用更新时导致错误。
外部内容FTS4表也可以使用RBU更新。在这种情况下,用户需要配置RBU数据库,以便将相同的UPDATE,DELETE和INSERT操作集应用于基础内容表的FTS4索引。对于外部内容FTS4表的所有更新,还要求用户确保将任何UPDATE或DELETE操作应用于FTS4索引,然后再将其应用于基础内容表(有关详细说明,请参阅FTS4文档)。在RBU中,这是通过确保用于写入FTS4表的data_%表的名称在用于使用BINARY更新基础内容表的data_%表的名称之前进行排序来完成的 整理顺序。为了避免在RBU数据库中重复数据,可以使用SQL视图代替data_%表之一。例如,对于目标数据库模式:
创建表ccc(addr,text); 使用fts4(addr,text,content = ccc)创建虚拟表ccc_fts;
可以使用以下RBU数据库架构:
创建表data_ccc(addr,text,rbu_rowid,rbu_control); 创建视图data0_ccc_fts AS SELECT * FROM data_ccc;
然后可以使用针对目标数据库表ccc的更新正常填充data_ccc表。RBU将从data0_ccc_fts视图读取相同的更新,并将其应用于FTS表ccc_fts。由于“ data0_ccc_fts”小于“ data_ccc”,因此将根据需要首先更新FTS表。
基础内容表具有显式INTEGER PRIMARY KEY列的情况稍微困难一些,因为对于FTS索引及其基础内容表,存储在rbu_control列中的文本值略有不同。对于基础内容表,对于显式IPK,任何rbu_control文本值中都必须包含一个字符,但对于具有隐式rowid的FTS表本身,则不应该包含字符。这很不方便,但是可以使用更复杂的视图来解决,如下所示:
-目标数据库架构 创建表ddd(i整数主键,k文本); 使用fts4(k,content = ddd)创建虚拟表ddd_fts; -RBU数据库架构 创建表data_ccc(i,k,rbu_control); 创建视图data0_ccc_fts AS SELECT i AS rbu_rowid,k,CASE 当rbu_control IN(0,1)时然后rbu_control ELSE substr(rbu_control,2)结束 FROM data_ccc;
上面的SQL视图中的substr()函数返回rbu_control参数的文本,其中删除了第一个字符(FTS表不需要该字符对应于列“ i”)。
从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_%表,该表的特征是具有行键的功能,该行键的功能类似于主键:
sqldiff --rbu --table <虚拟表名> t1.db t2.db
不幸的是,即使默认情况下虚拟表被忽略,它们为在数据库中存储数据而创建的任何 基础数据库表也不会被删除,并且sqldiff将包括将这些表添加到任何RBU数据库中。因此,尝试使用sqldiff创建RBU更新以应用于具有一个或多个虚拟表的目标数据库的用户可能必须使用--table选项单独运行sqldiff,以在目标数据库中更新每个表。
RBU扩展接口允许应用程序将存储在RBU数据库中的RBU更新应用于现有目标数据库。步骤如下:
使用sqlite3rbu_open(T,A,S)函数打开RBU句柄。
T参数是目标数据库文件的名称。A参数是RBU数据库文件的名称。S参数是“状态数据库”的名称,该状态数据库用于存储在中断后恢复更新所需的状态信息。S参数可以为NULL,在这种情况下,状态信息存储在RBU数据库的各种表中,这些表的名称均以“ rbu_”开头。
sqlite3rbu_open(T,A,S)函数返回一个指向“ sqlite3rbu”对象的指针,然后将该对象传递到后续接口中。
使用sqlite3rbu_db(X)返回的数据库句柄注册所有必需的虚拟表模块(其中参数X是从sqlite3rbu_open()返回的sqlite3rbu指针)。另外,如果需要,请使用sqlite3_create_function_v2()注册rbu_delta()SQL函数 。
在sqlite3rbu对象指针X上调用sqlite3rbu_step(X)函数一次或多次。每次调用sqlite3rbu_step()都会执行一次b树操作,因此可能需要成千上万次调用才能应用完整的更新。完全应用更新后,sqlite3rbu_step()接口将返回SQLITE_DONE。
调用sqlite3rbu_close(X)销毁sqlite3rbu对象指针。如果已经足够多次调用sqlite3rbu_step(X)来完全将更新应用于目标数据库,则将RBU数据库标记为已完全应用。否则,RBU更新应用程序的状态将保存在状态数据库中(如果sqlite3rbu_open()中的状态数据库文件的名称为NULL,则保存在RBU数据库中),以便以后恢复更新。
如果在调用sqlite3rbu_close()时仅将更新部分地应用于目标数据库,则状态信息会保存在状态数据库中(如果存在),否则会保存在RBU数据库中。这允许后续进程从上次中断的地方自动恢复RBU更新。如果状态信息存储在RBU数据库中,则可以通过删除名称以“ rbu_”开头的所有表来将其删除。
有关更多详细信息,请参见头文件sqlite3rbu.h中的注释 。
与SQLite的内置VACUUM命令相比,RBU Vacuum具有以下限制:
本节概述了示例代码,并演示了将RBU Vacuum集成到应用程序中的示例代码。有关完整的详细信息,请参见头文件sqlite3rbu.h中的注释 。
RBU Vacuum应用程序都实现了以下过程的一些变体:
通过调用sqlite3rbu_vacuum(T,S)创建RBU句柄。
参数T是要清除的数据库文件的名称。参数S是数据库的名称,如果真空操作被暂停,RBU模块将在该数据库中保存其状态。
如果在调用sqlite3rbu_vacuum()时状态数据库S不存在,则会自动创建状态数据库S并使用用于存储RBU真空状态的单个表“ rbu_state”进行填充。如果中止正在进行的RBU真空,则将使用状态数据填充该表。下次使用相同的S参数调用sqlite3rbu_vacuum()时,它将检测到该数据并尝试恢复暂停的真空操作。当RBU真空操作完成或遇到错误时,RBU会自动删除rbu_state表的内容。在这种情况下,对sqlite3rbu_vacuum()的下一次调用将从头开始全新的真空操作。
建立用于基于目标数据库名称确定RBU真空状态数据库名称的约定是一个好主意。下面的示例代码使用“ <target> -vacuum”,其中<target>是要清理的数据库的名称。
由sqlite3rbu_db()函数返回的两个数据库句柄都注册了要清理的数据库中的索引所使用的任何自定义排序顺序。
在RBU句柄上调用sqlite3rbu_step()函数,直到RBU真空结束,发生错误或应用程序希望暂停RBU真空为止。
每次调用sqlite3rbu_step()都会完成少量工作,以完成真空操作。根据数据库的大小,一次清理可能需要数千次调用sqlite3rbu_step()。如果真空操作已完成,则sqlite3rbu_step()返回SQLITE_DONE;如果真空操作尚未完成但未发生错误,则返回SQLITE_OK;如果遇到错误,则返回SQLite错误代码。如果确实发生错误,则对sqlite3rbu_step()的所有后续调用都会立即返回相同的错误代码。
最后,调用sqlite3rbu_close()关闭RBU句柄。如果应用程序在清理结束或发生错误之前停止调用sqlite3rbu_step(),则清理状态将保存在状态数据库中,以便以后可以恢复。
与sqlite3rbu_step()一样,如果真空操作已完成,则sqlite3rbu_close()返回SQLITE_DONE。如果清理尚未完成但没有发生错误,则返回SQLITE_OK。或者,如果发生错误,则返回SQLite错误代码。如果在先前调用sqlite3rbu_step()的过程中发生了错误,则sqlite3rbu_close()会返回相同的错误代码。
以下示例代码说明了上述技术。
/ * **在 **数据库zTarget 上启动新的RBU真空或恢复暂停的RBU真空。当发生错误,RBU **真空结束或应用程序发出中断 **(代码未显示)时返回。 ** **如果RBU清理成功完成,请返回SQLITE_DONE。 **如果发生错误,请返回SQLite错误代码。或者,如果应用程序 **发出中断信号,则暂停RBU真空操作,以便 可以通过随后对该函数的后续调用来恢复 它,并返回** SQLITE_OK。 ** **此函数将 状态数据库 使用名为“ <zTarget> -vacuum”的数据库,其中<zTarget>是要清理的数据库的名称**。 * / int do_rbu_vacuum(const char * zTarget){ int rc; char * zState; / *状态数据库的名称* / sqlite3rbu * pRbu; / * RBU真空手柄* / zState = sqlite3_mprintf(“%s-vacuum”,zTarget); if(zState == 0)返回SQLITE_NOMEM; pRbu = sqlite3rbu_vacuum(zTarget,zState); sqlite3_free(zState); if(pRbu){ sqlite3 * dbTarget = sqlite3rbu_db(pRbu,0); sqlite3 * dbState = sqlite3rbu_db(pRbu,1); / *目标数据库使用的任何自定义排序规则序列都必须 在这里向两个数据库句柄注册。* / while(sqlite3rbu_step(pRbu)== SQLITE_OK){ if(<应用程序已发出中断信号>)中断; } } rc = sqlite3rbu_close(pRbu); 返回rc; }