本文檔描述並定義了自 3.0.0 版 (2004-06-18) 以來所有 SQLite 版本使用的磁碟資料庫檔案格式。
SQLite 資料庫的完整狀態通常包含在磁碟上的一個檔案中,稱為「主要資料庫檔案」。
在交易期間,SQLite 會將額外資訊儲存在第二個檔案中,稱為「回滾日誌」,或者如果 SQLite 處於 WAL 模式,則儲存在預寫式日誌檔案中。
如果應用程式或主機電腦在交易完成之前當機,則回滾日誌或預寫式日誌包含將主要資料庫檔案還原到一致狀態所需的資訊。當回滾日誌或預寫式日誌包含恢復資料庫狀態所需的資訊時,它們稱為「熱日誌」或「熱 WAL 檔案」。熱日誌和 WAL 檔案僅在錯誤復原情況下才會成為一個因素,因此並不常見,但它們是 SQLite 資料庫狀態的一部分,因此不容忽視。本文檔定義了回滾日誌和預寫式日誌檔案的格式,但重點是主要資料庫檔案。
主要資料庫檔案由一或多個頁面組成。頁面大小是 512 到 65536 之間的 2 的次方數(含)。同一個資料庫中的所有頁面大小都相同。資料庫檔案的頁面大小由資料庫檔案開頭偏移 16 個位元組的 2 位元組整數決定。
頁碼從 1 開始編號。最大頁碼為 4294967294 (232 - 2)。最小的 SQLite 資料庫只有一個 512 位元組的頁面。最大的資料庫將會有 4294967294 個頁面,每個頁面 65536 位元組,總共 281,474,976,579,584 位元組(約 281 TB)。通常,SQLite 會在達到其內部大小限制之前,先達到底層檔案系統或磁碟硬體的最大檔案大小限制。
在一般使用情況下,SQLite 資料庫的大小通常介於幾 KB 到幾 GB 之間,儘管已知有 TB 級的 SQLite 資料庫在生產環境中運行。
在任何時間點,主資料庫中的每個頁面都具有單一用途,用途如下:
所有從主資料庫檔案的讀取和寫入都從頁面邊界開始,並且所有寫入的大小都是頁面數的整數倍。讀取的大小通常也是頁面數的整數倍,唯一的例外是當資料庫第一次打開時,資料庫檔案的前 100 個位元組(資料庫檔案標頭)會以子頁面大小單位讀取。
資料庫檔案的前 100 個位元組組成了資料庫檔案標頭。資料庫檔案標頭的欄位劃分如下表所示。資料庫檔案標頭中的所有多位元組欄位都以最高有效位元組優先(大端序)的順序儲存。
偏移量 | 大小 | 描述 |
---|---|---|
0 | 16 | 標頭字串:"SQLite format 3\000" |
16 | 2 | 資料庫頁面大小(以位元組為單位)。必須是 512 到 32768 之間的 2 的冪,或是值 1 代表頁面大小為 65536。 |
18 | 1 | 檔案格式寫入版本。1 代表舊版;2 代表 WAL。 |
19 | 1 | 檔案格式讀取版本。1 代表舊版;2 代表 WAL。 |
20 | 1 | 每個頁面末尾未使用的「保留」空間的位元組數。通常為 0。 |
21 | 1 | 最大嵌入式有效負載分數。必須為 64。 |
22 | 1 | 最小嵌入式有效負載分數。必須為 32。 |
23 | 1 | 葉子有效負載分數。必須為 32。 |
24 | 4 | 檔案變更計數器。 |
28 | 4 | 資料庫檔案大小(以頁面為單位)。「標頭中的資料庫大小」。 |
32 | 4 | 第一個可用空間清單樹幹頁面的頁碼。 |
36 | 4 | 可用空間清單頁面的總數。 |
40 | 4 | 結構描述 Cookie。 |
44 | 4 | 結構描述格式編號。支援的結構描述格式為 1、2、3 和 4。 |
48 | 4 | 預設頁面快取大小。 |
52 | 4 | 在自動真空或增量真空模式下,最大的根 B 樹頁面的頁碼,否則為零。 |
56 | 4 | 資料庫文字編碼。值 1 表示 UTF-8。值 2 表示 UTF-16le。值 3 表示 UTF-16be。 |
60 | 4 | 由 user_version pragma 讀取和設定的「使用者版本」。 |
64 | 4 | 增量真空模式為 True(非零)。否則為 False(零)。 |
68 | 4 | 由 PRAGMA application_id 設定的「應用程式 ID」。 |
72 | 20 | 保留供擴充使用。必須為零。 |
92 | 4 | 版本有效編號。 |
96 | 4 | SQLITE_VERSION_NUMBER |
每個有效的 SQLite 資料庫檔案都以以下 16 個位元組(十六進位制)開頭:53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00。此位元組序列對應於 UTF-8 字串「SQLite format 3」,包括結尾的空終止字元。
資料庫的頁面大小由從偏移量 16 開始的兩個位元組值決定。對於 SQLite 3.7.0.1 (2010-08-04) 及更早版本,此值被解釋為大端序整數,並且必須是 512 到 32768 之間(含)的 2 的冪次方。從 SQLite 3.7.1 版 (2010-08-23) 開始,支援 65536 位元組的頁面大小。由於 65536 無法容納在兩個位元組的整數中,因此要指定 65536 位元組的頁面大小,偏移量 16 處的值為 0x00 0x01。此值可以被解釋為大端序的 1,並視為表示 65536 頁面大小的魔術數字。或者,可以將這兩個位元組的欄位視為小端序數字,並表示它代表頁面大小除以 256。頁面大小欄位的這兩種解釋是等效的。
偏移量 18 和 19 處的檔案格式寫入版本和檔案格式讀取版本旨在允許在未來版本的 SQLite 中增強檔案格式。在目前的 SQLite 版本中,對於回滾日誌模式,這兩個值均為 1,對於 WAL 日誌模式,則為 2。如果根據當前檔案格式規範編碼的 SQLite 版本遇到讀取版本為 1 或 2 但寫入版本大於 2 的資料庫檔案,則必須將該資料庫檔案視為唯讀。如果遇到讀取版本大於 2 的資料庫檔案,則無法讀取或寫入該資料庫。
SQLite 能夠在每頁的末尾保留少量額外的位元組供擴充功能使用。例如,SQLite 加密擴充功能使用這些額外的位元組來儲存與每個頁面關聯的隨機數和/或加密校驗和。偏移量 20 處的 1 位元組整數中的「保留空間」大小是在每頁末尾為擴充功能保留的空間的位元組數。此值通常為 0。此值可以是奇數。
資料庫頁面的「可用大小」是標頭中偏移量 16 處的 2 位元組整數指定的頁面大小減去標頭中偏移量 20 處的 1 位元組整數記錄的「保留」空間大小。頁面的可用大小可能是一個奇數。但是,可用大小不能小於 480。換句話說,如果頁面大小為 512,則保留空間大小不能超過 32。
最大和最小嵌入式有效負載分數以及葉節點有效負載分數值必須分別為 64、32 和 32。這些值最初旨在作為可調整參數,可用於修改 B 樹演算法的儲存格式。但是,該功能不受支援,目前也沒有在未來新增支援的計畫。因此,這三個位元組固定為指定的值。
檔案變更計數器是偏移量 24 處的一個 4 位元組大端序整數,每當資料庫檔案在修改後解鎖時,該計數器就會遞增。當兩個或多個行程正在讀取同一個資料庫檔案時,每個行程都可以透過監視變更計數器來偵測其他行程的資料庫變更。由於快取已過時,因此行程通常會在另一個行程修改資料庫時清除其資料庫頁面快取。檔案變更計數器有助於實現此目的。
在 WAL 模式下,使用 wal-index 偵測資料庫的變更,因此不需要變更計數器。因此,在 WAL 模式下,變更計數器可能不會在每個交易中遞增。
標頭中偏移量為 28 的 4 位元組大端序整數儲存資料庫檔案的大小(以頁面為單位)。如果此標頭內資料大小無效(請參閱下一段),則資料庫大小會透過查看資料庫檔案的實際大小來計算。舊版 SQLite 會忽略標頭內資料庫大小,並僅使用實際檔案大小。新版 SQLite 會使用標頭內資料庫大小(如果可用),但如果標頭內資料庫大小無效,則會改用實際檔案大小。
僅當標頭內資料庫大小非零,且偏移量為 24 的 4 位元組更改計數器與偏移量為 92 的 4 位元組版本有效編號完全相符時,標頭內資料庫大小才視為有效。當資料庫僅使用最新版本的 SQLite(版本 3.7.0(2010-07-21)及更高版本)修改時,標頭內資料庫大小始終有效。如果舊版 SQLite 寫入資料庫,它將不知道要更新標頭內資料庫大小,因此標頭內資料庫大小可能不正確。但舊版 SQLite 也會使偏移量為 92 的版本有效編號保持不變,因此它將與更改計數器不匹配。因此,可以透過觀察更改計數器與版本有效編號不匹配的情況來檢測(並忽略)無效的標頭內資料庫大小。
資料庫檔案中未使用的頁面儲存在可用頁面清單中。偏移量為 32 的 4 位元組大端序整數儲存可用頁面清單第一頁的頁碼,如果可用頁面清單為空,則為零。偏移量為 36 的 4 位元組大端序整數儲存可用頁面清單中的頁面總數。
結構描述 Cookie 是偏移量為 40 的 4 位元組大端序整數,每當資料庫結構描述變更時,它就會遞增。已準備好的陳述式是針對特定版本的資料庫結構描述進行編譯的。當資料庫結構描述變更時,必須重新準備陳述式。當已準備好的陳述式執行時,它會先檢查結構描述 Cookie,以確保其值與準備陳述式時的值相同,如果結構描述 Cookie 已變更,則陳述式會自動重新準備並重新執行,或者中止並顯示 SQLITE_SCHEMA 錯誤。
結構描述格式編號是偏移量為 44 的 4 位元組大端序整數。結構描述格式編號類似於偏移量為 18 和 19 的檔案格式讀取和寫入版本號,不同之處在於結構描述格式編號指的是高階 SQL 格式,而不是低階 B 樹格式。目前定義了四種結構描述格式編號
SQLite 建立的新資料庫檔案預設使用格式 4。可以使用 legacy_file_format pragma 使 SQLite 使用格式 1 建立新的資料庫檔案。可以透過在編譯時設定 SQLITE_DEFAULT_FILE_FORMAT=1,將格式版本號的預設值設為 1 而不是 4。
如果資料庫完全空白,如果它沒有結構描述,則結構描述格式編號可以為零。
在偏移量 48 處的 4 位元組大端序有號整數是資料庫檔案的建議快取大小(以頁面為單位)。此值僅為建議值,SQLite 並沒有義務遵循它。此整數的絕對值會被用作建議的大小。建議的快取大小可以使用 default_cache_size pragma 設定。
在偏移量 52 和 64 處的兩個 4 位元組大端序整數用於管理 auto_vacuum 和 incremental_vacuum 模式。如果偏移量 52 處的整數為零,則指標映射 (ptrmap) 頁面會從資料庫檔案中省略,並且不支援 auto_vacuum 和 incremental_vacuum。如果偏移量 52 處的整數非零,則它是資料庫檔案中最大根頁面的頁碼,資料庫檔案將包含 ptrmap 頁面,並且模式必須是 auto_vacuum 或 incremental_vacuum。在後一種情況下,偏移量 64 處的整數對於 incremental_vacuum 為真,對於 auto_vacuum 為假。如果偏移量 52 處的整數為零,則偏移量 64 處的整數也必須為零。
在偏移量 56 處的 4 位元組大端序整數決定了儲存在資料庫中所有文字字串的編碼。值 1 表示 UTF-8。值 2 表示 UTF-16le。值 3 表示 UTF-16be。不允許其他值。 sqlite3.h 標頭檔定義了 C 預處理器巨集 SQLITE_UTF8 為 1,SQLITE_UTF16LE 為 2,SQLITE_UTF16BE 為 3,以代替文字編碼的數字代碼。
在偏移量 60 處的 4 位元組大端序整數是使用者版本,可由 user_version pragma 設定和查詢。SQLite 不使用使用者版本。
在偏移量 68 處的 4 位元組大端序整數是一個「應用程式 ID」,可以由 PRAGMA application_id 命令設定,以便將資料庫識別為屬於或與特定應用程式相關聯。應用程式 ID 適用於用作 應用程式檔案格式 的資料庫檔案。應用程式 ID 可以被諸如 file(1) 之類的工具程式使用,以確定特定的檔案類型,而不是僅僅回報「SQLite3 資料庫」。可以通過查閱 SQLite 原始程式碼儲存庫中的 magic.txt 檔案來查看已分配的應用程式 ID 列表。
在偏移量 96 處的 4 位元組大端序整數儲存最近修改資料庫檔案的 SQLite 程式庫的 SQLITE_VERSION_NUMBER 值。在偏移量 92 處的 4 位元組大端序整數是儲存版本號時的 變更計數器 的值。偏移量 92 處的整數指示版本號對哪個事務有效,有時稱為「版本有效號」。
資料庫檔案標頭的所有其他位元組都保留用於未來擴充,並且必須設定為零。
鎖定位元組頁面是資料庫檔案的單個頁面,其中包含偏移量在 1073741824 到 1073742335 之間(含)的位元組。大小小於或等於 1073741824 位元組的資料庫檔案不包含鎖定位元組頁面。大於 1073741824 位元組的資料庫檔案恰好包含一個鎖定位元組頁面。
鎖定位元組頁面是預留給作業系統特定的 虛擬檔案系統 (VFS) 實作,用於實現資料庫檔案鎖定原語。SQLite 不使用鎖定位元組頁面。SQLite 核心永遠不會讀取或寫入鎖定位元組頁面,但作業系統特定的 VFS 實作可能會根據底層系統的需求和傾向選擇讀取或寫入鎖定位元組頁面上的位元組。內建於 SQLite 的 Unix 和 Win32 VFS 實作不會寫入鎖定位元組頁面,但其他作業系統的第三方 VFS 實作可能會。
鎖定位元組頁面的出現是由於需要支援 Win95,Win95 是設計此檔案格式時的主要作業系統,並且只支援強制檔案鎖定。據我們所知,所有現代作業系統都支援建議檔案鎖定,因此不再真正需要鎖定位元組頁面,但為了向後相容性而保留。
資料庫檔案可能包含一個或多個未使用的頁面。例如,從資料庫中刪除資訊時,可能會產生未使用的頁面。未使用的頁面會儲存在閒置頁面串列中,並在需要額外頁面時重新使用。
閒置頁面串列的組織方式是一個閒置頁面串列主幹頁面的鏈結串列,每個主幹頁面都包含零個或多個閒置頁面串列葉頁面的頁碼。
閒置頁面串列主幹頁面由一個 4 位元組大端序整數陣列組成。陣列的大小是盡可能容納在頁面可用空間中的整數數量。最小可用空間為 480 位元組,因此陣列的長度將始終至少為 120 個項目。閒置頁面串列主幹頁面上的第一個整數是串列中下一個閒置頁面串列主幹頁面的頁碼,如果這是最後一個閒置頁面串列主幹頁面,則為零。閒置頁面串列主幹頁面上的第二個整數是後續葉頁面指標的數量。將閒置頁面串列主幹頁面上的第二個整數稱為 L。如果 L 大於零,則陣列索引介於 2 和 L+1(含)之間的整數包含閒置頁面串列葉頁面的頁碼。
閒置頁面串列葉頁面不包含任何資訊。SQLite 避免讀取或寫入閒置頁面串列葉頁面,以減少磁碟 I/O。
在 3.6.0 (2008-07-16) 之前的 SQLite 版本中,如果閒置頁面串列主幹頁面陣列中的最後 6 個項目中的任何一個包含非零值,則會導致資料庫被報告為損毀。較新版本的 SQLite 沒有這個問題。但是,較新版本的 SQLite 仍然避免使用閒置頁面串列主幹頁面陣列中的最後六個項目,以便較舊版本的 SQLite 可以讀取較新版本的 SQLite 建立的資料庫檔案。
閒置頁面串列的頁面數量儲存為一個 4 位元組大端序整數,位於資料庫標頭中,距檔案開頭偏移 36 位元組。資料庫標頭也將第一個閒置頁面串列主幹頁面的頁碼儲存為一個 4 位元組大端序整數,距檔案開頭偏移 32 位元組。
B 樹演算法在面向頁面的儲存裝置上提供具有唯一且有序鍵的鍵/值儲存。有關 B 樹的背景資訊,請參閱 Knuth 的《電腦程式設計藝術》第 3 卷「排序與搜尋」,第 471-479 頁。SQLite 使用兩種 B 樹變體。「資料表 B 樹」使用 64 位元有號整數鍵,並將所有資料儲存在葉節點中。「索引 B 樹」使用任意鍵,並且根本不儲存資料。
B 樹頁面可以是內部頁面或葉頁面。葉頁面包含鍵,如果是資料表 B 樹,則每個鍵都有關聯的資料。內部頁面包含 K 個鍵以及 K+1 個指向子 B 樹頁面的指標。內部 B 樹頁面中的「指標」只是子頁面的 32 位元無號整數頁碼。
一個內部 B 樹頁面上的鍵值數量 K 幾乎總是至少為 2,通常遠大於 2。唯一的例外是當頁面 1 作為內部 B 樹頁面時。由於該頁面開頭存在資料庫標頭,頁面 1 可用的儲存空間減少了 100 位元組,因此有時(很少見)如果頁面 1 是內部 B 樹頁面,它最終可能只包含一個鍵值。在所有其他情況下,K 為 2 或更大。K 的上限是頁面上所能容納的最多鍵值數量。索引 B 樹上的大型鍵值會被拆分到溢位頁面中,這樣任何單個鍵值都不會使用超過頁面上可用儲存空間的四分之一,因此每個內部頁面都能夠儲存至少 4 個鍵值。資料表 B 樹的整數鍵值永遠不會大到需要溢位,因此鍵值溢位只會發生在索引 B 樹上。
葉子 B 樹的深度定義為 1,任何內部 B 樹的深度定義為其子樹最大深度加 1。在結構良好的資料庫中,內部 B 樹的所有子樹都具有相同的深度。
在內部 B 樹頁面中,指標和鍵值邏輯上交替排列,兩端都是指標。(前一句應從概念上理解——頁面中鍵值和指標的實際佈局更為複雜,將在後續描述。)同一頁面中的所有鍵值都是唯一的,並且邏輯上從左到右按升序排列。(同樣,此排序是邏輯上的,而不是物理上的。鍵值在頁面中的實際位置是任意的。)對於任何鍵值 X,X 左側的指標指向所有鍵值小於或等於 X 的 B 樹頁面。X 右側的指標指向所有鍵值大於 X 的頁面。
在內部 B 樹頁面中,每個鍵值及其緊鄰左側的指標組合成一個稱為「單元格 (cell)」的結構。最右側的指標單獨存放。葉子 B 樹頁面沒有指標,但它仍然使用單元格結構來存放索引 B 樹的鍵值或資料表 B 樹的鍵值和內容。資料也包含在單元格中。
每個 B 樹頁面最多有一個父 B 樹頁面。沒有父頁面的 B 樹頁面稱為根頁面。根 B 樹頁面及其所有子樹的閉包構成一個完整的 B 樹。一個完整的 B 樹可能(實際上也很常見)只包含一個頁面,該頁面既是葉子又是根。由於存在從父頁面到子頁面的指標,因此只要知道根頁面,就可以找到完整 B 樹的每個頁面。因此,B 樹由其根頁面號碼標識。
B 樹頁面可以是資料表 B 樹頁面,也可以是索引 B 樹頁面。每個完整 B 樹中的所有頁面都屬於同一類型:資料表或索引。資料庫檔案中,對於資料庫結構描述中的每個 rowid 資料表(包括 sqlite_schema 等系統資料表),都有一個資料表 B 樹。資料庫檔案中,對於結構描述中的每個索引(包括由唯一性約束建立的隱含索引),都有一個索引 B 樹。沒有與 虛擬資料表 關聯的 B 樹。特定的虛擬資料表實現可能會使用 影子資料表 進行儲存,但這些影子資料表在資料庫結構描述中會有單獨的條目。WITHOUT ROWID 資料表使用索引 B 樹而不是資料表 B 樹,因此資料庫檔案中,對於每個 WITHOUT ROWID 資料表,都有一個索引 B 樹。對應於 sqlite_schema 資料表的 B 樹始終是資料表 B 樹,並且其根頁面始終為 1。sqlite_schema 資料表包含資料庫檔案中所有其他資料表和索引的根頁面號碼。
表格 B-tree 中的每個條目都包含一個 64 位元帶符號整數鍵值,以及最多 2147483647 位元組的任意數據。(表格 B-tree 的鍵值對應於 B-tree 所實現的 SQL 表格的 rowid。)內部表格 B-tree 只儲存鍵值和指向子節點的指標。所有數據都包含在表格 B-tree 的葉節點中。
索引 B-tree 中的每個條目都包含一個最多 2147483647 位元組長度的任意鍵值,不包含數據。
將儲存格的「有效負載 (payload)」定義為儲存格中任意長度的部分。對於索引 B-tree,鍵值始終是任意長度的,因此有效負載就是鍵值。內部表格 B-tree 頁面中的儲存格沒有任意長度的元素,因此這些儲存格沒有有效負載。表格 B-tree 葉節點頁面包含任意長度的內容,因此對於這些頁面上的儲存格,有效負載就是內容。
當儲存格的有效負載大小超過特定閾值(稍後定義)時,則只有有效負載的前幾個位元組儲存在 B-tree 頁面上,其餘部分儲存在內容溢位頁面的鏈結串列中。
B-tree 頁面按以下順序劃分區域:
100 位元組的資料庫檔案標頭僅在第 1 頁出現,該頁面始終是表格 B-tree 頁面。資料庫檔案中的所有其他 B-tree 頁面都省略此 100 位元組標頭。
保留區域是每個頁面(鎖定頁面除外)末尾的未使用空間區域,擴充功能可以使用該區域來儲存每頁資訊。保留區域的大小由資料庫檔案標頭偏移量 20 處的一個位元組無符號整數決定。保留區域的大小通常為零。
B-tree 頁面標頭對於葉節點頁面大小為 8 位元組,對於內部節點頁面大小為 12 位元組。頁面標頭中的所有多位元組值皆為大端序 (big-endian)。B-tree 頁面標頭由以下欄位組成:
偏移量 | 大小 | 描述 |
---|---|---|
0 | 1 | 偏移量 0 處的一個位元組旗標,指示 B-tree 頁面類型。
|
1 | 2 | 偏移量 1 處的兩個位元組整數表示頁面上第一個可用區塊的起始位置,如果沒有可用區塊,則為零。 |
3 | 2 | 偏移量 3 處的兩個位元組整數表示頁面上的儲存格數量。 |
5 | 2 | 偏移量 5 處的兩個位元組整數指定儲存格內容區域的起始位置。此整數的零值被解釋為 65536。 |
7 | 1 | 偏移量 7 處的一個位元組整數表示儲存格內容區域內的碎片可用位元組數。 |
8 | 4 | 偏移量 8 處的四個位元組頁碼是最右指標。此值僅出現在內部 B-tree 頁面的標頭中,所有其他頁面都省略此值。 |
B-tree 頁面的儲存格指標陣列緊跟在 B-tree 頁面標頭之後。假設 K 是 B-tree 上的儲存格數量。儲存格指標陣列由 K 個指向儲存格內容的 2 位元組整數偏移量組成。儲存格指標按鍵值順序排列,最左邊的儲存格(鍵值最小的儲存格)在前,最右邊的儲存格(鍵值最大的儲存格)在後。
儲存格內容儲存在 B-tree 頁面的儲存格內容區域中。SQLite 盡可能將儲存格放置在 B-tree 頁面的末尾,以便為儲存格指標陣列的未來增長留出空間。最後一個儲存格指標陣列條目和第一個儲存格開頭之間的區域是未配置區域。
如果一個頁面不包含任何 Cell(這只可能發生在不包含任何列的表格的根頁面),則指向 Cell 內容區域的偏移量將等於頁面大小減去保留空間的位元組數。如果資料庫使用 65536 位元組的頁面大小,且保留空間為零(保留空間的常用值),則空頁面的 Cell 內容偏移量應為 65536。然而,該整數太大而無法儲存在 2 位元組的無符號整數中,因此使用值 0 來代替。
Freeblock 是一種用於識別 B 樹頁面中未配置空間的結構。Freeblock 組織成一個鏈。Freeblock 的前 2 個位元組是一個大端序整數,它是鏈中下一個 freeblock 在 B 樹頁面中的偏移量,如果 freeblock 是鏈中的最後一個,則為零。每個 freeblock 的第三和第四個位元組形成一個大端序整數,它是 freeblock 的大小(以位元組為單位),包括 4 位元組的標頭。Freeblock 總是按偏移量遞增的順序連接。B 樹頁面標頭的第二個欄位是第一個 freeblock 的偏移量,如果頁面上沒有 freeblock,則為零。在格式良好的 B 樹頁面中,在第一個 freeblock 之前總是至少有一個 Cell。
一個 freeblock 需要至少 4 個位元組的空間。如果在 Cell 內容區域內有一組孤立的 1、2 或 3 個未使用的位元組,則這些位元組組成一個片段。所有片段中的位元組總數儲存在 B 樹頁面標頭的第五個欄位中。在格式良好的 B 樹頁面中,片段中的位元組總數不得超過 60。
B 樹頁面上的可用空間總量由未配置區域的大小加上所有 freeblock 的總大小加上片段化可用位元組的數量組成。SQLite 可能會不時重新組織 B 樹頁面,以便沒有 freeblock 或片段位元組,所有未使用的位元組都包含在未配置空間區域中,並且所有 Cell 都緊密排列在頁面末尾。這稱為 B 樹頁面的「碎片整理」。
可變長度整數或「Varint」是 64 位元二補數整數的靜態霍夫曼編碼,它對較小的正值使用較少的空間。Varint 的長度介於 1 到 9 個位元組之間。Varint 由零個或多個高位元組設定的位元組,後跟一個高位元組清除的位元組,或九個位元組(以較短者為準)組成。前八個位元組中每個位元組的低七位和第九個位元組的所有 8 位元用於重建 64 位元二補數整數。Varint 是大端序:從 Varint 的較早位元組獲取的位元比從較晚位元組獲取的位元更重要。
Cell 的格式取決於 Cell 出現在哪種 B 樹頁面上。下表顯示了各種 B 樹頁面類型中 Cell 的元素,按出現順序排列。
表格 B 樹葉節點 Cell(標頭 0x0d)
表格 B 樹內部節點 Cell(標頭 0x05)
索引 B 樹葉節點 Cell(標頭 0x0a)
索引 B 樹內部節點 Cell(標頭 0x02)
以上資訊可以重新整理成表格格式,如下所示
資料類型 | 出現於... | 描述 | |||
---|---|---|---|---|---|
表格葉節點 (0x0d) | 表格內部節點 (0x05) | 索引葉節點 (0x0a) | 索引內部節點 (0x02) | ||
4 位元組整數 | ✔ | ✔ | 左子節點的頁碼 | ||
變長整數 (Varint) | ✔ | ✔ | ✔ | 有效負載的位元組數 | |
變長整數 (Varint) | ✔ | ✔ | 列 ID (Rowid) | ||
位元組陣列 | ✔ | ✔ | ✔ | 有效負載 | |
4 位元組整數 | ✔ | ✔ | ✔ | 第一個溢位頁面的頁碼 |
溢位到溢位頁面的有效負載量也取決於頁面類型。在以下計算中,設 U 為資料庫頁面的可用大小,即頁面總大小減去每個頁面末尾的保留空間。設 P 為有效負載大小。以下符號 X 代表可以直接儲存在 B 樹頁面上而不溢位到溢位頁面的最大有效負載量,符號 M 代表允許溢位之前必須儲存在 B 樹頁面上的最小有效負載量。
表格 B 樹葉節點儲存格
設 X 為 U-35。如果有效負載大小 P 小於或等於 X,則整個有效負載儲存在 B 樹葉節點頁面上。設 M 為 ((U-12)*32/255)-23,設 K 為 M+((P-M)%(U-4))。如果 P 大於 X,則儲存在表格 B 樹葉節點頁面上的位元組數為 K(如果 K 小於或等於 X)或 M(否則)。儲存在葉節點頁面上的位元組數永遠不小於 M。
表格 B 樹內部節點儲存格
表格 B 樹的內部頁面沒有有效負載,因此永遠不會有任何有效負載溢位。
索引 B 樹葉節點或內部節點儲存格
設 X 為 ((U-12)*64/255)-23。如果有效負載大小 P 小於或等於 X,則整個有效負載儲存在 B 樹頁面上。設 M 為 ((U-12)*32/255)-23,設 K 為 M+((P-M)%(U-4))。如果 P 大於 X,則儲存在索引 B 樹頁面上的位元組數為 K(如果 K 小於或等於 X)或 M(否則)。儲存在索引頁面上的位元組數永遠不小於 M。
以下是相同計算的另一種描述
溢位閾值的設計是為了給索引 B 樹提供最小扇出數 4,並確保 B 樹頁面上有足夠的有效負載,以便通常無需查閱溢位頁面即可存取記錄標頭。事後看來,SQLite B 樹邏輯的設計者意識到這些閾值可以更簡化。但是,如果更改計算方式,將導致檔案格式不相容。而且目前的計算方式運作良好,即使它們有點複雜。
當 B 樹儲存格的有效負載對於 B 樹頁面而言過大時,多餘的部分會溢位到溢位頁面。溢位頁面形成一個連結串列。每個溢位頁面的前四個位元組是一個大端序整數,它是鏈中下一頁的頁碼,鏈中最後一頁的頁碼為零。從第五個位元組到最後一個可用位元組用於儲存溢位內容。
指標映射 (Ptrmap) 頁面是插入資料庫的額外頁面,用於提高自動真空和增量真空模式的效率。資料庫中的其他頁面類型通常具有從父節點到子節點的指標。例如,內部 B 樹頁面包含指向其子 B 樹頁面的指標,溢位鏈具有從鏈中較早連結到較晚連結的指標。Ptrmap 頁面包含反方向的連結資訊,即從子節點到父節點。
任何資料庫檔案,如果在資料庫標頭偏移量 52 處的「最大根 B 樹頁面」值非零,則必須存在 Ptrmap 頁面。如果「最大根 B 樹頁面」值為零,則資料庫中不得包含 Ptrmap 頁面。
在具有 Ptrmap 頁面的資料庫中,第一個 Ptrmap 頁面是第 2 頁。Ptrmap 頁面由一個 5 位元組項目的陣列組成。設 J 為頁面可用空間中可容納的 5 位元組項目的數量。(換句話說,J=U/5。)第一個 Ptrmap 頁面將包含第 3 頁到第 J+2 頁(含)的反向指標資訊。第二個 Ptrmap 頁面將位於第 J+3 頁,該 Ptrmap 頁面將提供第 J+4 頁到第 2*J+3 頁(含)的反向指標資訊。以此類推,涵蓋整個資料庫檔案。
在使用 Ptrmap 頁面的資料庫中,所有位於上一段計算所識別位置的頁面都必須是 Ptrmap 頁面,其他任何頁面都不能是 Ptrmap 頁面。例外情況是,如果位元組鎖定頁面恰好與 Ptrmap 頁面位於相同的頁碼上,則在這種情況下,Ptrmap 會被移到下一頁。
Ptrmap 頁面上的每個 5 位元組項目都提供關於緊跟在該指標映射之後的其中一個頁面的反向連結資訊。如果頁面 B 是一個 Ptrmap 頁面,則關於頁面 B+1 的反向連結資訊由指標映射上的第一個項目提供。關於頁面 B+2 的資訊由第二個項目提供。以此類推。
每個 5 位元組的 Ptrmap 項目由一個位元組的「頁面類型」資訊和一個 4 位元組的大端序頁碼組成。識別五種頁面類型:
在任何包含 Ptrmap 頁面的資料庫檔案中,所有 B 樹根頁面都必須位於任何非根 B 樹頁面、儲存格有效負載溢位頁面或可用空間列表頁面之前。此限制可確保在自動真空或增量真空期間根頁面永遠不會被移動。自動真空邏輯不知道如何更新 sqlite_schema 表的 root_page 欄位,因此有必要防止在自動真空期間移動根頁面,以保持 sqlite_schema 表的完整性。CREATE TABLE、CREATE INDEX、DROP TABLE 和 DROP INDEX 操作會將根頁面移動到資料庫檔案的開頭。
前文描述了 SQLite 檔案格式的低階層面。B 樹機制提供了一種存取大型資料集的強大而有效的方法。本節將描述如何使用低階 B 樹層來實現更高階的 SQL 功能。
上文將表 B 樹葉頁面的資料和索引 B 樹頁面的鍵值描述為任意位元組序列。先前的討論提到了一個鍵值小於另一個鍵值,但沒有定義「小於」的含義。本節將解決這些遺漏。
有效負載(表 B 樹資料或索引 B 樹鍵值)始終採用「記錄格式」。記錄格式定義了與表或索引中的欄位對應的一系列值。記錄格式指定了欄位的數量、每個欄位的資料類型以及每個欄位的內容。
記錄格式廣泛使用上面定義的 64 位元帶符號整數的可變長度整數或varint表示法。
一條記錄依序包含標頭和主體。標頭以單個 varint 開頭,該 varint 決定標頭中的總位元組數。varint 值是標頭的大小(以位元組為單位),包括大小 varint 本身。在大小 varint 之後是一個或多個額外的 varint,每個欄位一個。這些額外的 varint 稱為「序列類型」編號,並根據下表確定每個欄位的資料類型。
序列類型 | 內容大小 | 意義 |
---|---|---|
0 | 0 | 值為 NULL。 |
1 | 1 | 值為 8 位元二補數整數。 |
2 | 2 | 值為大端序 16 位元二補數整數。 |
3 | 3 | 值為大端序 24 位元二補數整數。 |
4 | 4 | 值為大端序 32 位元二補數整數。 |
5 | 6 | 值為大端序 48 位元二補數整數。 |
6 | 8 | 值為大端序 64 位元二補數整數。 |
7 | 8 | 值為大端序 IEEE 754-2008 64 位元浮點數。 |
8 | 0 | 值為整數 0。(僅適用於綱要格式 4 及更高版本。) |
9 | 0 | 值為整數 1。(僅適用於綱要格式 4 及更高版本。) |
10,11 | 可變 | 保留供內部使用。這些序列類型代碼永遠不會出現在格式正確的資料庫檔案中,但它們可能用於 SQLite 有時為自身使用而產生的暫時和臨時資料庫檔案中。這些代碼的含義可能會因 SQLite 的不同版本而有所不同。 |
N≥12 且為偶數 | (N-12)/2 | 值是一個長度為 (N-12)/2 位元組的 BLOB。 |
N≥13 且為奇數 | (N-13)/2 | 值是以文字編碼表示的字串,長度為 (N-13)/2 位元組。不儲存 nul 終止符。 |
標頭大小 varint 和序列類型 varint 通常由單個位元組組成。大型字串和 BLOB 的序列類型 varint 可能會擴展到兩個或三個位元組的 varint,但這是例外而不是規則。varint 格式在編碼記錄標頭方面非常有效。
記錄中每個欄位的值緊跟在標頭之後。對於序列類型 0、8、9、12 和 13,該值長度為零位元組。如果所有欄位都屬於這些類型,則記錄的主體部分為空。
一條記錄的值可能少於相應表格中的欄位數。例如,在 ALTER TABLE ... ADD COLUMN SQL 陳述式增加了表格綱要中的欄位數但未修改表格中先前存在的列之後,可能會發生這種情況。記錄末尾缺少的值會使用表格綱要中定義的相應欄位的預設值來填補。
索引 B 樹中鍵的順序由鍵所代表的記錄的排序順序決定。記錄比較逐欄進行。記錄的欄位從左到右檢查。第一對不相等的欄位決定了兩條記錄的相對順序。個別欄位的排序順序如下:
為了計算文字欄位的順序,每個欄位都需要一個排序規則函式。SQLite 定義了三個內建的排序規則函式:
BINARY 內建的 BINARY 排序規則使用標準 C 函式庫中的 memcmp() 函式逐位元組比較字串。 NOCASE NOCASE 排序規則類似於 BINARY,不同之處在於大寫 ASCII 字元('A' 到 'Z')在進行比較之前會先轉換為小寫。只有 ASCII 字元會進行大小寫轉換。NOCASE 並未實現通用的 Unicode 無大小寫比較。 RTRIM RTRIM 類似於 BINARY,不同之處在於任一字串尾端的額外空格不會改變結果。換句話說,只要字串之間的差異僅在於尾端空格的數量,它們就會被視為相同。
可以使用 sqlite3_create_collation() 介面將其他應用程式特定的排序規則函式新增到 SQLite。
所有字串的預設排序規則函式是 BINARY。表格欄位的替代排序規則函式可以在 CREATE TABLE 陳述式中使用 欄位定義 上的 COLLATE 子句來指定。當欄位被索引時,預設情況下,索引中該欄位會使用在 CREATE TABLE 陳述式中指定的相同排序規則函式,但這可以使用 CREATE INDEX 陳述式中的 COLLATE 子句來覆寫。
資料庫綱要中的每個一般 SQL 表格都以表格 B 樹狀結構表示在磁碟上。表格 B 樹狀結構中的每個項目對應於 SQL 表格中的一列。SQL 表格的 rowid 是表格 B 樹狀結構中每個項目的 64 位元帶正負號整數鍵值。
每個 SQL 表格列的內容儲存在資料庫檔案中,方法是先將各欄位中的值組合成記錄格式的位元組陣列,然後將該位元組陣列儲存為表格 B 樹狀結構中項目的有效負載。記錄中值的順序與 SQL 表格定義中欄位的順序相同。當 SQL 表格包含 INTEGER PRIMARY KEY 欄位(作為 rowid 的別名)時,該欄位在記錄中會顯示為 NULL 值。SQLite 在參考 INTEGER PRIMARY KEY 欄位時,將永遠使用表格 B 樹狀結構鍵值,而不是 NULL 值。
如果欄位的 關聯性 (affinity) 為 REAL,且該欄位包含可以無損轉換為整數的值(如果該值不包含小數部分,且大小足以表示為整數),則該欄位可能會在記錄中儲存為整數。SQLite 從記錄中提取值時,會將值轉換回浮點數。
如果 SQL 表格是在其 CREATE TABLE 陳述式結尾使用 "WITHOUT ROWID" 子句建立的,則該表格是 WITHOUT ROWID 表格,並使用不同的磁碟上表示方式。WITHOUT ROWID 表格使用索引 B 樹狀結構而非表格 B 樹狀結構進行儲存。WITHOUT ROWID B 樹狀結構中每個項目的鍵值是由 PRIMARY KEY 的欄位以及表格中所有剩餘欄位組成的記錄。主鍵欄位按照它們在 PRIMARY KEY 子句中宣告的順序出現,而剩餘欄位則按照它們在 CREATE TABLE 陳述式中出現的順序出現。
因此,WITHOUT ROWID 表格的內容編碼與一般 rowid 表格的內容編碼相同,不同之處在於欄位的順序會重新排列,使 PRIMARY KEY 欄位排在最前面,而且內容會用作索引 B 樹狀結構中的鍵值,而不是表格 B 樹狀結構中的資料。適用於 REAL 關聯性欄位的特殊編碼規則,同樣適用於 WITHOUT ROWID 表格和 rowid 表格。
如果 WITHOUT ROWID 表的 PRIMARY KEY 使用相同排序序列的相同欄位超過一次,則 PRIMARY KEY 定義中該欄位的第二次和後續出現將被忽略。例如,以下所有 CREATE TABLE 陳述式都指定相同的表,它們在磁碟上的表示方式完全相同
CREATE TABLE t1(a,b,c,d,PRIMARY KEY(a,c)) WITHOUT ROWID; CREATE TABLE t1(a,b,c,d,PRIMARY KEY(a,c,a,c)) WITHOUT ROWID; CREATE TABLE t1(a,b,c,d,PRIMARY KEY(a,A,a,C)) WITHOUT ROWID; CREATE TABLE t1(a,b,c,d,PRIMARY KEY(a,a,a,a,c)) WITHOUT ROWID;
當然,上面的第一個例子是表格的首選定義。所有範例都建立了一個 WITHOUT ROWID 表格,其中包含兩個 PRIMARY KEY 欄位「a」和「c」(按此順序),後跟兩個數據欄位「b」和「d」(也按此順序)。
每個 SQL 索引,無論是透過 CREATE INDEX 陳述式明確宣告,還是由 UNIQUE 或 PRIMARY KEY 約束隱含,都對應於資料庫檔案中的一個索引 B 樹。索引 B 樹中的每個條目都對應於相關聯的 SQL 表格中的一列。索引 B 樹的鍵是由被索引的欄位組成,後跟對應表格列的鍵的記錄。對於普通表格,列鍵是 rowid,而對於 WITHOUT ROWID 表格,列鍵是 PRIMARY KEY。因為表格中的每一列都有一個唯一的列鍵,所以索引中的所有鍵都是唯一的。
在一般索引中,表格中的列與該表格關聯的每個索引中的條目之間存在一對一的映射關係。然而,在 部分索引 中,索引 B 樹僅包含對應於 CREATE INDEX 陳述式上的 WHERE 子句表達式為真的表格列的條目。索引和表格 B 樹中對應的列共享相同的 rowid 或主鍵值,並且所有索引欄位的值都相同。
在 WITHOUT ROWID 表格的索引中,如果 PRIMARY KEY 的一個欄位也是索引中的一個欄位,並且具有匹配的排序序列,則索引欄位不會在索引記錄末尾的表格鍵後綴中重複。例如,請考慮以下 SQL
CREATE TABLE ex25(a,b,c,d,e,PRIMARY KEY(d,c,a)) WITHOUT rowid; CREATE INDEX ex25ce ON ex25(c,e); CREATE INDEX ex25acde ON ex25(a,c,d,e); CREATE INDEX ex25ae ON ex25(a COLLATE nocase,e);
ex25ce 索引中的每一列都是一條包含以下欄位的記錄:c、e、d、a。前兩欄是被索引的欄位 c 和 e。其餘欄位是對應表格列的主鍵。通常,主鍵會是欄位 d、c 和 a,但由於欄位 c 已經出現在索引的前面,因此會從鍵後綴中省略。
在被索引的欄位涵蓋 PRIMARY KEY 的所有欄位的極端情況下,索引將僅包含被索引的欄位。上面的 ex25acde 範例演示了這一點。ex25acde 索引中的每個條目僅包含欄位 a、c、d 和 e,按此順序排列。
ex25ae 中的每一列都包含五個欄位:a、e、d、c、a。「a」欄位重複出現,因為第一次出現的「a」的排序函數是「nocase」,而第二次出現的「a」的排序序列是「binary」。如果「a」欄位沒有重複,並且表格包含兩個或多個具有相同「e」值的條目,而「a」僅在大小寫上有所不同,則所有這些表格條目都將對應於索引中的一個條目,這將破壞表格和索引之間的一對一對應關係。
僅在 WITHOUT ROWID 表格中才會抑制索引條目鍵後綴中的冗餘欄位。在普通的 rowid 表格中,索引條目始終以 rowid 結尾,即使 INTEGER PRIMARY KEY 欄位是被索引的欄位之一。
資料庫檔案的第 1 頁是一個表格 B 樹的根頁面,該表格 B 樹包含一個名為「sqlite_schema」的特殊表格。這個 B 樹被稱為「結構綱要表格」,因為它儲存了完整的資料庫結構綱要。sqlite_schema 表格的結構就像使用以下 SQL 建立的一樣
CREATE TABLE sqlite_schema( type text, name text, tbl_name text, rootpage integer, sql text );
sqlite_schema 表格包含資料庫結構描述中每個表格、索引、檢視和觸發器(統稱為「物件」)的一列資料,但 sqlite_schema 表格本身沒有項目。除了應用程式和程式設計師定義的物件之外,sqlite_schema 表格還包含內部結構描述物件的項目。
sqlite_schema.type 欄位將根據所定義物件的類型,包含以下其中一個文字字串:'table'、'index'、'view' 或 'trigger'。'table' 字串同時用於一般表格和虛擬表格。
sqlite_schema.name 欄位將包含物件的名稱。表格上的 UNIQUE 和 PRIMARY KEY 約束會導致 SQLite 建立名為「sqlite_autoindex_TABLE_N」形式的內部索引,其中 TABLE 會被包含約束的表格名稱取代,而 N 是一個從 1 開始的整數,並且隨著表格定義中看到的每個約束遞增。在 WITHOUT ROWID 表格中,PRIMARY KEY 沒有 sqlite_schema 項目,但「sqlite_autoindex_TABLE_N」名稱會被保留給 PRIMARY KEY,如同 sqlite_schema 項目存在一樣。這將影響後續 UNIQUE 約束的編號。「sqlite_autoindex_TABLE_N」名稱永遠不會分配給 INTEGER PRIMARY KEY,無論是在 rowid 表格還是 WITHOUT ROWID 表格中。
sqlite_schema.tbl_name 欄位包含與物件關聯的表格或檢視的名稱。對於表格或檢視,tbl_name 欄位是 name 欄位的副本。對於索引,tbl_name 是被索引的表格的名稱。對於觸發器,tbl_name 欄位儲存觸發觸發器的表格或檢視的名稱。
sqlite_schema.rootpage 欄位儲存表格和索引的根 b 樹頁面的頁碼。對於定義檢視、觸發器和虛擬表格的列,rootpage 欄位為 0 或 NULL。
sqlite_schema.sql 欄位儲存描述物件的 SQL 文字。此 SQL 文字是一個 CREATE TABLE、CREATE VIRTUAL TABLE、CREATE INDEX、CREATE VIEW 或 CREATE TRIGGER 陳述式,如果在資料庫檔案作為 資料庫連線 的主要資料庫時對其進行評估,則會重新建立該物件。該文字通常是建立物件時使用的原始陳述式的副本,但會套用正規化,使文字符合以下規則
sqlite_schema.sql 欄位中的文字是建立物件的原始 CREATE 陳述式文字的副本,但會如上所述進行正規化,並由後續的 ALTER TABLE 陳述式修改。對於由 UNIQUE 或 PRIMARY KEY 約束自動建立的內部索引,sqlite_schema.sql 為 NULL。
名稱「sqlite_schema」不會出現在檔案格式中的任何位置。該名稱只是資料庫實作所使用的慣例。由於歷史和操作上的考量,「sqlite_schema」表格有時也可以使用以下其中一個別名來稱呼
由於結構描述表格的名稱不會出現在檔案格式中的任何位置,因此如果應用程式選擇使用這些替代名稱之一來參考結構描述表格,則資料庫檔案的含義不會改變。
除了應用程式和/或開發人員使用 CREATE 語句 SQL 建立的表格、索引、視圖和觸發器之外,sqlite_schema 表格中還可能包含零個或多個由 SQLite 為了內部使用而建立的內部結構物件的項目。內部結構物件的名稱一律以 "sqlite_" 開頭,任何名稱以 "sqlite_" 開頭的表格、索引、視圖或觸發器都是內部結構物件。SQLite 禁止應用程式建立名稱以 "sqlite_" 開頭的物件。
SQLite 使用的內部結構物件可能包含以下幾項:
名稱格式為 "sqlite_autoindex_TABLE_N" 的索引,用於實作一般表格上的 UNIQUE 和 PRIMARY KEY 限制。
名稱為 "sqlite_sequence" 的表格,用於追蹤使用 AUTOINCREMENT 的表格的最大歷史 INTEGER PRIMARY KEY 值。
名稱格式為 "sqlite_statN" 的表格,其中 N 為整數。此類表格儲存由 ANALYZE 命令收集的資料庫統計資訊,並由查詢規劃器用於協助判斷每個查詢的最佳演算法。
未來版本可能會在 SQLite 檔案格式中新增以 "sqlite_" 開頭的新的內部結構物件名稱。
sqlite_sequence 表格是一個內部表格,用於協助實作 AUTOINCREMENT。每當建立具有 AUTOINCREMENT 整數主鍵的普通表格時,就會自動建立 sqlite_sequence 表格。一旦建立,sqlite_sequence 表格將永久存在於 sqlite_schema 表格中;它無法被刪除。sqlite_sequence 表格的結構如下:
CREATE TABLE sqlite_sequence(name,seq);
sqlite_sequence 表格中,每個使用 AUTOINCREMENT 的普通表格都有一列對應的資料。表格的名稱(如同在 sqlite_schema.name 中顯示的)位於 sqlite_sequence.name 欄位中,而曾經插入該表格的最大 INTEGER PRIMARY KEY 值則位於 sqlite_sequence.seq 欄位中。AUTOINCREMENT 表格新自動產生的整數主鍵保證會大於該表格的 sqlite_sequence.seq 欄位值。如果 AUTOINCREMENT 表格的 sqlite_sequence.seq 欄位已達到最大整數值 (9223372036854775807),則嘗試向該表格新增具有自動產生整數主鍵的新列將會失敗,並出現 SQLITE_FULL 錯誤。當向 AUTOINCREMENT 表格插入新項目時,sqlite_sequence.seq 欄位會根據需要自動更新。刪除 AUTOINCREMENT 表格時,該表格對應的 sqlite_sequence 列也會自動刪除。如果更新 AUTOINCREMENT 表格時,該表格對應的 sqlite_sequence 列不存在,則會建立新的 sqlite_sequence 列。如果 AUTOINCREMENT 表格的 sqlite_sequence.seq 值被手動設定為非整數值,並且後續嘗試插入或更新 AUTOINCREMENT 表格,則其行為未定義。
應用程式程式碼可以修改 sqlite_sequence 表格,新增列、刪除列或修改現有列。但是,如果 sqlite_sequence 表格不存在,應用程式程式碼則無法建立它。應用程式程式碼可以刪除 sqlite_sequence 表格中的所有項目,但不能刪除 sqlite_sequence 表格本身。
sqlite_stat1 表格是由 ANALYZE 命令建立的內部表格,用於儲存關於表格和索引的補充資訊,查詢規劃器可以使用這些資訊來協助找到更好的查詢執行方式。應用程式可以更新、刪除、插入或刪除 sqlite_stat1 表格,但不能建立或修改 sqlite_stat1 表格。sqlite_stat1 表格的結構如下:
CREATE TABLE sqlite_stat1(tbl,idx,stat);
通常每個索引對應一列,索引名稱由 sqlite_stat1.idx 欄位標識。 sqlite_stat1.tbl 欄位是指索引所屬的表格名稱。在每一列中,sqlite_stat.stat 欄位是一個字串,包含一串整數,後接零個或多個參數。列表中的第一個整數是索引中大約的行數。(索引中的行數與表格中的行數相同,除了部分索引之外。)第二個整數是索引中第一欄值相同的近似行數。第三個整數是索引中前兩欄值相同的行數。第 N 個整數 (N>1) 是索引中前 N-1 欄值相同的估計平均行數。對於一個 K 欄索引,stat 欄位將包含 K+1 個整數。如果索引是唯一的,則最後一個整數將是 1。
stat 欄位中的整數列表之後可以選擇性地加上參數,每個參數都是一個不包含空格的字元序列。所有參數前都有一個空格。無法識別的參數將被忽略。
如果存在 "unordered" 參數,則查詢規劃器會假設索引是無序的,並且不會將索引用於範圍查詢或排序。
"sz=NNN" 參數(其中 NNN 表示一個或多個數字的序列)表示表格或索引中所有記錄的平均行大小為每行 NNN 位元組。SQLite 查詢規劃器可能會使用 "sz=NNN" 標記提供的估計行大小資訊來幫助它選擇較小的表格和索引,以減少磁碟 I/O。
索引的 sqlite_stat1.stat 欄位中存在 "noskipscan" 標記會阻止該索引與跳躍掃描優化一起使用。
在 SQLite 未來增強功能中,可能會在 stat 欄位的末尾添加新的文字標記。為了相容性,stat 欄位末尾無法識別的標記將被忽略。
如果 sqlite_stat1.idx 欄位為 NULL,則 sqlite_stat1.stat 欄位包含一個整數,該整數是由 sqlite_stat1.tbl 標識的表格中的大約行數。如果 sqlite_stat1.idx 欄位與 sqlite_stat1.tbl 欄位相同,則該表格是一個WITHOUT ROWID表格,sqlite_stat1.stat 欄位包含有關實現 WITHOUT ROWID 表格的索引 btree 的資訊。
僅當使用 SQLITE_ENABLE_STAT2 編譯 SQLite 且 SQLite 版本號介於 3.6.18 (2009-09-11) 和 3.7.8 (2011-09-19) 之間時,才會建立和使用 sqlite_stat2。 3.6.18 之前的任何版本的 SQLite 或 3.7.8 之後的版本都不會讀取或寫入 sqlite_stat2 表格。 sqlite_stat2 表格包含有關索引內鍵值分佈的其他資訊。 sqlite_stat2 表格的結構如下:
CREATE TABLE sqlite_stat2(tbl,idx,sampleno,sample);
sqlite_stat2 表格的每一列中的 sqlite_stat2.idx 欄位和 sqlite_stat2.tbl 欄位標識該列所描述的索引。 sqlite_stat2 表格中通常每個索引有 10 列。
sqlite_stat2.sampleno 介於 0 到 9(含)之間的索引的 sqlite_stat2 項目是索引中最左邊鍵值的樣本,這些樣本是在索引上均勻分佈的點取得的。假設 C 是索引中的行數。則取樣的列由以下公式給出:
列號 = (i*C*2 + C)/20
上式中的變數 i 從 0 到 9 不等。從概念上講,索引空間被劃分為 10 個均勻的桶,樣本是每個桶的中間列。
此處記錄 sqlite_stat2 的格式僅供歷史參考。最新版本的 SQLite 不再支援 sqlite_stat2,並且如果存在 sqlite_stat2 表格,則會被忽略。
只有當 SQLite 編譯時使用了 SQLITE_ENABLE_STAT3 或 SQLITE_ENABLE_STAT4,且 SQLite 版本號為 3.7.9 (2011-11-01) 或更高版本時,才會使用 sqlite_stat3 表格。在 3.7.9 之前的任何 SQLite 版本都不會讀取或寫入 sqlite_stat3 表格。如果使用了 SQLITE_ENABLE_STAT4 編譯時選項,且 SQLite 版本號為 3.8.1 (2013-10-17) 或更高版本,則可能會讀取 sqlite_stat3,但不會寫入。sqlite_stat3 表格包含索引鍵值分佈的額外資訊,查詢規劃器可以使用這些資訊來設計更好、更快的查詢演算法。sqlite_stat3 表格的結構如下:
CREATE TABLE sqlite_stat3(tbl,idx,nEq,nLt,nDLt,sample);
對於每個索引,sqlite_stat3 表格中通常有多個項目。sqlite_stat3.sample 欄位儲存由 sqlite_stat3.idx 和 sqlite_stat3.tbl 標識的索引最左欄位的值。sqlite_stat3.nEq 欄位儲存索引中最左欄位與樣本完全匹配的項目數的近似值。sqlite_stat3.nLt 欄位儲存索引中最左欄位小於樣本的項目數的近似值。sqlite_stat3.nDLt 欄位儲存索引中最左欄位小於樣本的不同項目數的近似值。
每個索引可以有任意數量的 sqlite_stat3 項目。ANALYZE 命令通常會產生包含 10 到 40 個樣本的 sqlite_stat3 表格,這些樣本分佈在鍵值空間中,且具有較大的 nEq 值。
在結構良好的 sqlite_stat3 表格中,任何單一索引的樣本必須按照它們在索引中出現的順序排列。換句話說,如果最左欄位為 S1 的項目在索引 B 樹中比最左欄位為 S2 的項目更早出現,則在 sqlite_stat3 表格中,樣本 S1 的 rowid 必須小於樣本 S2 的 rowid。
只有當 SQLite 編譯時使用了 SQLITE_ENABLE_STAT4,且 SQLite 版本號為 3.8.1 (2013-10-17) 或更高版本時,才會建立並使用 sqlite_stat4 表格。在 3.8.1 之前的任何 SQLite 版本都不會讀取或寫入 sqlite_stat4 表格。sqlite_stat4 表格包含索引鍵值分佈或 WITHOUT ROWID 表格主鍵鍵值分佈的額外資訊。查詢規劃器有時可以使用 sqlite_stat4 表格中的額外資訊來設計更好、更快的查詢演算法。sqlite_stat4 表格的結構如下:
CREATE TABLE sqlite_stat4(tbl,idx,nEq,nLt,nDLt,sample);
對於每個有統計資訊的索引,sqlite_stat4 表格中通常有 10 到 40 個項目,但這些限制並非硬性規定。sqlite_stat4 表格中各欄位的含義如下:
tbl | sqlite_stat4.tbl 欄位儲存擁有此列所描述索引的表格名稱。 |
idx | sqlite_stat4.idx 欄位儲存此列所描述的索引名稱,如果是 WITHOUT ROWID 表格的 sqlite_stat4 項目,則儲存表格本身的名稱。 |
sample | sqlite_stat4.sample 欄位儲存一個 記錄格式 的 BLOB,該 BLOB 編碼了索引欄位,後跟 rowid 表格的 rowid 或 WITHOUT ROWID 表格的主鍵欄位。WITHOUT ROWID 表格本身的 sqlite_stat4.sample BLOB 只包含主鍵欄位。假設 sqlite_stat4.sample blob 編碼的欄位數為 N。對於普通 rowid 表格的索引,N 將比索引欄位數多一個。對於 WITHOUT ROWID 表格的索引,N 將是索引欄位數加上主鍵欄位數。對於 WITHOUT ROWID 表格,N 將是主鍵欄位數。 |
nEq | sqlite_stat4.nEq 欄位包含一個 N 個整數的列表,其中第 K 個整數是指索引中其最左邊 K 個欄位與樣本最左邊 K 個欄位完全相符的項目數量近似值。 |
nLt | sqlite_stat4.nLt 欄位包含一個 N 個整數的列表,其中第 K 個整數是指索引中其最左邊 K 個欄位集體小於樣本最左邊 K 個欄位的項目數量近似值。 |
nDLt | sqlite_stat4.nDLt 欄位包含一個 N 個整數的列表,其中第 K 個整數是指索引中最左邊 K 個欄位相異,且其最左邊 K 個欄位集體小於樣本最左邊 K 個欄位的項目數量近似值。 |
sqlite_stat4 表是 sqlite_stat3 表的泛化。sqlite_stat3 表提供索引最左邊欄位的資訊,而 sqlite_stat4 表提供索引所有欄位的資訊。
每個索引可以有任意數量的 sqlite_stat4 項目。 ANALYZE 命令通常會產生包含 10 到 40 個樣本的 sqlite_stat4 表,這些樣本分佈在鍵值空間中,且具有較大的 nEq 值。
在格式良好的 sqlite_stat4 表中,任何單一索引的樣本必須以它們在索引中出現的相同順序出現。換句話說,如果項目 S1 在索引 B 樹中比項目 S2 更早出現,那麼在 sqlite_stat4 表中,樣本 S1 的 rowid 必須小於樣本 S2 的 rowid。
復原日誌是一個與每個 SQLite 資料庫檔案關聯的檔案,其中包含用於在事務過程中將資料庫檔案恢復到初始狀態的資訊。復原日誌檔案始終位於與資料庫檔案相同的目錄中,並且與資料庫檔案具有相同的名稱,但會附加字串「-journal」。一個資料庫只能關聯一個復原日誌,因此一次只能對一個資料庫開啟一個寫入事務。
在修改資料庫的任何資訊頁面之前,該頁面的原始未修改內容會寫入復原日誌。如果事務中斷且需要復原,則可以使用復原日誌將資料庫恢復到其原始狀態。可用空間鏈結葉頁面不包含需要在復原時恢復的資訊,因此為了減少磁碟 I/O,它們在修改之前不會寫入日誌。
如果由於應用程式當機、單一或作業系統當機,或硬體電源故障或當機而導致事務中止,則主資料庫檔案可能會處於不一致的狀態。下次 SQLite 嘗試開啟資料庫檔案時,將會偵測到復原日誌檔案的存在,並且會自動播放該日誌以將資料庫恢復到未完成事務開始時的狀態。
只有當復原日誌存在且包含有效的標頭時,它才被視為有效。因此,可以透過三種方式之一提交事務:
這三種提交事務的方式分別對應於 journal_mode pragma 的 DELETE、TRUNCATE 和 PERSIST 設定。
有效的復原日誌以以下格式的標頭開頭:
偏移量 | 大小 | 描述 |
---|---|---|
0 | 8 | 標頭字串:0xd9, 0xd5, 0x05, 0xf9, 0x20, 0xa1, 0x63, 0xd7 |
8 | 4 | 「頁面計數」- 日誌下一段中的頁面數,或 -1 表示到檔案結尾的所有內容 |
12 | 4 | 用於檢查碼的隨機數值 (nonce) |
16 | 4 | 資料庫的初始大小(以頁面為單位) |
20 | 4 | 寫入此日誌的行程所假設的磁碟扇區大小。 |
24 | 4 | 此日誌中頁面的大小。 |
復原日誌標頭以零填充到單個扇區的大小(由偏移量 20 處的扇區大小整數定義)。標頭位於一個獨立的扇區中,以便在寫入扇區時發生電源損失的情況下,標頭後面的資訊將(希望)不會損壞。
在標頭和零填充之後是零個或多個頁面記錄。每個頁面記錄儲存資料庫檔案中頁面在更改前的內容副本。同一個頁面在單個復原日誌中可能不會出現多次。要復原未完成的交易,行程只需從頭到尾讀取復原日誌,並將日誌中找到的頁面寫回到資料庫檔案中的適當位置。
假設資料庫頁面大小(日誌標頭中偏移量 24 處的整數值)為 N。則頁面記錄的格式如下:
偏移量 | 大小 | 描述 |
---|---|---|
0 | 4 | 資料庫檔案中的頁碼 |
4 | N | 交易開始前頁面的原始內容 |
N+4 | 4 | 檢查碼 |
檢查碼是一個無符號的 32 位元整數,其計算方式如下:
檢查碼值用於防止在電源故障後不完整地寫入日誌頁面記錄。每次啟動交易時都會使用不同的隨機隨機數,以盡量減少未寫入的扇區可能偶然包含來自先前日誌的同一頁面資料的風險。透過為每個交易更改隨機數,磁碟上的過時資料仍將產生不正確的檢查碼,並以高機率被檢測到。由於效能原因,檢查碼僅使用資料記錄中 32 位元字的稀疏樣本 - 在 SQLite 3.0.0 的規劃階段進行的設計研究表明,對整個頁面進行檢查碼計算會導致顯著的效能下降。
假設日誌標頭中偏移量 8 處的頁面計數值為 M。如果 M 大於零,則在 M 個頁面記錄之後,日誌檔案可能會以零填充到扇區大小的下一個倍數,並且可能會插入另一個日誌標頭。同一日誌中的所有日誌標頭必須包含相同的資料庫頁面大小和扇區大小。
如果初始日誌標頭中的 M 為 -1,則後續頁面記錄的數量是透過計算日誌檔案剩餘可用空間中可以容納多少頁面記錄來計算的。
從 3.7.0 版(2010-07-21)開始,SQLite 支援一種稱為「預寫式日誌」或「WAL」的新交易控制機制。當資料庫處於 WAL 模式時,所有與該資料庫的連線都必須使用 WAL。特定資料庫將使用復原日誌或 WAL,但不會同時使用兩者。WAL 總是位於與資料庫檔案相同的目錄中,並且與資料庫檔案具有相同的名稱,但會附加字串「-wal」。
一個 WAL 檔案 由一個標頭和零個或多個「框架」組成。每個框架記錄了資料庫檔案中單一頁面的修訂內容。所有對資料庫的更改都藉由將框架寫入 WAL 來記錄。當寫入包含提交標記的框架時,交易即提交。單個 WAL 可以且通常會記錄多個交易。系統會定期將 WAL 的內容傳輸回資料庫檔案,這個操作稱為「檢查點」。
單個 WAL 檔案可以重複使用多次。換句話說,WAL 可以填滿框架,然後建立檢查點,新的框架可以覆蓋舊的框架。WAL 總是從頭到尾增長。附加到每個框架的檢查碼和計數器用於確定 WAL 中哪些框架有效,哪些是先前檢查點的殘留物。
WAL 標頭大小為 32 位元組,由以下八個大端序 (Big-Endian) 的 32 位元無符號整數值組成:
偏移量 | 大小 | 描述 |
---|---|---|
0 | 4 | 魔術數字:0x377f0682 或 0x377f0683 |
4 | 4 | 檔案格式版本:目前為 3007000。 |
8 | 4 | 資料庫頁面大小:例如 1024 |
12 | 4 | 檢查點序列號 |
16 | 4 | Salt-1:隨每個檢查點遞增的隨機整數 |
20 | 4 | Salt-2:每個檢查點不同的隨機數 |
24 | 4 | Checksum-1:標頭前 24 位元組的檢查碼的第一部分 |
28 | 4 | Checksum-2:標頭前 24 位元組的檢查碼的第二部分 |
緊跟在 wal-header 之後的是零個或多個框架。每個框架由一個 24 位元組的框架標頭和 _頁面大小_ 位元組的頁面資料組成。框架標頭是六個大端序的 32 位元無符號整數值,如下所示:
偏移量 | 大小 | 描述 |
---|---|---|
0 | 4 | 頁碼 |
4 | 4 | 對於提交記錄,提交後資料庫檔案的大小(以頁面為單位)。對於所有其他記錄,則為零。 |
8 | 4 | 從 WAL 標頭複製的 Salt-1 |
12 | 4 | 從 WAL 標頭複製的 Salt-2 |
16 | 4 | Checksum-1:累計檢查碼,包含此頁面 |
20 | 4 | Checksum-2:累計檢查碼的第二部分。 |
只有在滿足以下條件時,框架才被視為有效:
框架標頭中的 salt-1 和 salt-2 值與 wal-header 中的 salt 值匹配
框架標頭中最後 8 位元組的檢查碼值與連續計算 WAL 標頭的前 24 位元組、所有框架的前 8 位元組以及直到並包含當前框架的內容所得到的檢查碼完全匹配。
檢查碼的計算方法是將輸入解釋為偶數個無符號 32 位元整數:x(0) 到 x(N)。如果 WAL 標頭的前 4 個位元組中的魔術數字是 0x377f0683,則 32 位元整數為大端序;如果魔術數字是 0x377f0682,則為小端序。無論使用哪種位元組順序來計算檢查碼,檢查碼值始終以大端序格式儲存在框架標頭中。
檢查碼演算法僅適用於長度為 8 位元組倍數的內容。換句話說,如果輸入是 x(0) 到 x(N),則 N 必須是奇數。檢查碼演算法如下:
s0 = s1 = 0 for i from 0 to n-1 step 2: s0 += x(i) + s1; s1 += x(i+1) + s0; endfor # result in s0 and s1
輸出 s0 和 s1 都是使用反向斐波那契權重的加權檢查碼。(最大的斐波那契權重出現在被加總序列的第一個元素上。)s1 值涵蓋序列的所有 32 位元整數項,而 s0 省略最後一項。
在 檢查點 上,首先使用 VFS 的 xSync 方法將 WAL 刷新到永久儲存區。然後將 WAL 的有效內容傳輸到資料庫檔案中。最後,使用另一個 xSync 方法呼叫將資料庫刷新到永久儲存區。xSync 操作充當寫入屏障 - 在 xSync 開始之前啟動的所有寫入操作必須在 xSync 之後啟動的任何寫入操作開始之前完成。
檢查點不必完整執行。有可能某些讀取器仍在使用較舊的交易,其資料包含在資料庫檔案中。在這種情況下,將較新交易的內容從 WAL 檔案傳輸到資料庫會刪除仍在使用較舊交易的讀取器底下的內容。為了避免這種情況,只有當所有讀取器都使用 WAL 中的最後一個交易時,檢查點才會完整執行。
在完成檢查點後,如果沒有其他連線正在使用 WAL 的交易中,則後續的寫入交易可以從頭覆寫 WAL 檔案。這稱為「重設 WAL」。在第一個新的寫入交易開始時,WAL 標頭的 salt-1 值會遞增,而 salt-2 值會隨機化。這些 salt 值的更改會使 WAL 中已檢查點但尚未被覆寫的舊框架失效,並防止它們再次被檢查點。
WAL 檔案可以在重設時選擇性地截斷,但並非必須如此。如果 WAL 沒有被截斷,效能通常會好一些,因為檔案系統通常覆寫現有檔案的速度比擴展檔案的速度快。
要從資料庫讀取頁面(稱為頁碼 P),讀取器首先會檢查 WAL 是否包含頁面 P。如果是,則頁面 P 的最後一個有效實例(其後接著一個提交框架或是提交框架本身)將成為讀取的值。如果 WAL 不包含任何有效的頁面 P 副本,且這些副本是提交框架或後跟提交框架,則會從資料庫檔案讀取頁面 P。
要啟動讀取交易,讀取器會記錄 WAL 中的值框架數量為「mxFrame」。(更多細節) 讀取器將此記錄的 mxFrame 值用於所有後續的讀取操作。新的交易可以附加到 WAL,但只要讀取器使用其原始的 mxFrame 值並忽略後續附加的內容,讀取器就會看到資料庫在單個時間點的一致快照。這種技術允許多個並行讀取器同時檢視資料庫內容的不同版本。
前述段落中的讀取器演算法可以正常運作,但由於頁面 P 的框架可能出現在 WAL 中的任何位置,讀取器必須掃描整個 WAL 來尋找頁面 P 框架。如果 WAL 很大(通常是數百萬位元組),則掃描可能會很慢,讀取效能也會受到影響。為了克服這個問題,會維護一個稱為 wal-index 的獨立資料結構,以加快搜尋特定頁面的框架。
WAL-index 在概念上是共享記憶體,雖然目前的 VFS 實作使用記憶體映射檔案來實現作業系統的可攜性。記憶體映射檔案與資料庫位於相同的目錄中,且檔案名稱與資料庫相同,只是附加了「-shm」後綴。由於 wal-index 是共享記憶體,因此當用戶端位於不同的機器上時,SQLite 不支援在網路檔案系統上使用 journal_mode=WAL,因為資料庫的所有用戶端都必須能夠共享相同的記憶體。
wal-index 的目的是快速回答這個問題:
給定頁碼 P 和最大 WAL 框架索引 M,傳回頁面 P 的最大 WAL 框架索引(不超過 M),或者如果沒有頁面 P 的框架不超過 M,則傳回 NULL。
前一段落中的 *M* 值是在交易開始時讀取的 第 4.4 節 中定義的「mxFrame」值,它定義了讀取器將使用的 WAL 中的最大框架。
wal 索引檔是暫時的。當系統崩潰後,wal 索引檔會從原始的 WAL 檔案重建。當最後一個連線關閉時,VFS 需要截斷或歸零 wal 索引檔的標頭。由於 wal 索引檔是暫時的,它可以使用特定於架構的格式;它不需要跨平台。因此,不像資料庫和 WAL 檔案格式將所有值儲存為大端序 (big-endian),wal 索引檔以主機電腦的原生位元組順序儲存多位元組值。
本文件關注的是資料庫檔案的持久狀態,而由於 wal 索引檔是一個暫時性結構,因此這裡不會提供有關 wal 索引檔格式的更多資訊。wal 索引檔格式的更多細節包含在單獨的 WAL 索引檔格式 文件中。
本頁面最後修改時間:UTC 2024-07-24 10:03:34