小巧、快速、可靠。
任選三項。
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 樹都得到最佳的更新,來解決這個問題。對於大型資料庫檔案(無法放入作業系統磁碟快取的檔案),這個程序可以讓更新速度快上兩個數量級。

    RBU Vacuum 作業需要的暫時磁碟空間較少,而且寫入磁碟的資料量也比 SQLite VACUUM 少。SQLite VACUUM 需要大約最終資料庫檔案大小兩倍的暫時磁碟空間才能執行。寫入的資料總量大約是最終資料庫檔案大小的三倍。相比之下,RBU Vacuum 需要大約最終資料庫檔案大小的暫時磁碟空間,而且寫入磁碟的總量是這個大小的兩倍。

    另一方面,RBU Vacuum 使用的 CPU 比一般的 SQLite VACUUM 多,在一個測試中,使用量多達五倍。因此,在相同條件下,RBU Vacuum 通常會比 SQLite VACUUM 慢很多。

  2. RBU 在背景中執行

    正在進行的 RBU 作業(更新或 Vacuum)不會干擾對資料庫檔案的讀取存取。

  3. RBU 以增量方式執行

    RBU 作業可能會暫停,然後稍後繼續,可能會有電源中斷和/或系統重設。對於 RBU 更新,原始資料庫內容在整個更新套用之前,對所有資料庫讀取器仍然可見 - 即使更新已暫停,然後稍後繼續。

RBU 擴充套件在預設情況下未啟用。若要啟用它,請使用 合併 編譯 SQLITE_ENABLE_RBU 編譯時間選項。

2. RBU 更新

2.1. RBU 更新限制

下列限制適用於 RBU 更新

2.2. 準備 RBU 更新檔案

RBU 要套用的所有變更都會儲存在一個名為「RBU 資料庫」的獨立 SQLite 資料庫中。要修改的資料庫稱為「目標資料庫」。

對於目標資料庫中會由更新修改的每個資料表,都會在 RBU 資料庫中建立一個對應的資料表。RBU 資料庫資料表結構與目標資料庫不同,但會根據 以下說明 從目標資料庫衍生而來。

RBU 資料庫資料表會針對目標資料庫中由更新新增、更新或刪除的每列資料,包含一列資料。在 以下區段 中說明如何填入 RBU 資料庫資料表。

2.2.1. RBU 資料庫結構

對於目標資料庫中的每個資料表,RBU 資料庫應包含一個名為「data<整數>_<目標資料表名稱>」的資料表,其中 <目標資料表名稱> 是目標資料庫中資料表的名稱,而 <整數> 是由零個或多個數字字元 (0-9) 組成的任何順序。RBU 資料庫中的資料表會依名稱順序處理 (根據 BINARY 排序順序從最小到最大),因此更新目標資料表的順序會受到 data_% 資料表名稱的 <整數> 部分的選取影響。雖然這在使用 RBU 更新 特定類型的虛擬資料表 時很有用,但通常沒有理由不使用空白字串取代 <整數>。

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 更新,否則不會。例如,要寫入具有隱藏語言識別碼欄位的 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。在此情況下,新列會靜默取代具有相同主鍵值的任何現有列。這等同於刪除後再使用相同的主鍵值進行 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 值。化石增量資料會儲存在 data_% 表格中,而不是儲存新的 BLOB。而且,在要更新的欄位的 rbu_control 字串中,不會指定「x」,而是儲存「f」字元。在處理「f」更新時,RBU 會從磁碟載入原始的 BLOB 資料,套用化石增量資料,然後將結果儲存回資料庫檔案。由 sqldiff --rbu 產生的 RBU 資料庫會在任何可以節省 RBU 資料庫空間的地方使用化石增量資料。

若要使用客製化增量資料格式,RBU 應用程式必須在開始處理更新之前,註冊一個使用者定義的 SQL 函式,名稱為「rbu_delta」。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;

如果目標資料庫表格是虛擬表格或沒有 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 宣告。但是,如果從每個 data_% 表格中以「rowid」順序讀取列與依據對應目標資料庫表格的 PRIMARY KEY 排序讀取列大致相同,則 RBU 會更有效率。換句話說,在將列插入 data_% 表格之前,應使用目標表格 PRIMARY KEY 欄位對列進行排序。

2.2.3. 將 RBU 與 FTS3/4 表格搭配使用

通常,FTS3 或 FTS4 表格是具有類似於 PRIMARY KEY 的 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);

並填充,就像目標表格是沒有明確 PRIMARY KEY 欄位的普通 SQLite 表格一樣。

無內容的 FTS4 表格 的處理方式類似,只不過任何嘗試更新或刪除列的動作都會在套用更新時導致錯誤。

外部內容 FTS4 表格 也可使用 RBU 更新。在此情況下,使用者必須設定 RBU 資料庫,以便將同一組 UPDATE、DELETE 和 INSERT 作業套用至 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 索引及其基礎內容表格略有不同。對於基礎內容表格,必須在任何 rbu_control 文字值中包含明確 IPK 的字元,但對於具有隱式 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 引數的文字,並移除第一個字元(對應於 FTS 表格不需要的「i」欄)。

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 是要進行 Vacuum 的資料庫檔案名稱。引數 S 是 RBU 模組在 Vacuum 作業暫停時將其狀態儲存其中的資料庫名稱。

    當呼叫 sqlite3rbu_vacuum() 時,如果狀態資料庫 S 不存在,系統會自動建立該資料庫,並使用單一表格「rbu_state」儲存 RBU 清空狀態。如果正在進行的 RBU 清空作業暫停,這個表格會填入狀態資料。下次使用相同的 S 參數呼叫 sqlite3rbu_vacuum() 時,它會偵測這些資料,並嘗試繼續暫停的清空作業。當 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;如果清空作業尚未完成,但沒有發生錯誤,sqlite3rbu_step() 會傳回 SQLITE_OK;如果遇到錯誤,sqlite3rbu_step() 會傳回 SQLite 錯誤碼。如果確實發生錯誤,後續所有呼叫 sqlite3rbu_step() 都會立即傳回相同的錯誤碼。

  4. 最後,呼叫 sqlite3rbu_close() 來關閉 RBU 句柄。如果應用程式在真空完成或發生錯誤之前停止呼叫 sqlite3rbu_step(),真空的狀態會儲存在狀態資料庫中,以便稍後繼續。

    與 sqlite3rbu_step() 相同,如果真空作業已完成,sqlite3rbu_close() 會傳回 SQLITE_DONE。如果真空尚未完成,但未發生錯誤,會傳回 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