SQLite 資料庫檔案格式
SQLite 資料庫檔案,一個自給自足的嵌入式 SQL 資料庫。
- MIME 類型
- application/vnd.sqlite3
- 副檔名
- .sqlite, .db, .sqlite3
- 分類
- 資料與資料庫
Magic number(檔案簽章)
| 0x00000000 | 53514C69746520666F726D6174203300 | SQLite format 3· | 'SQLite format 3\0' header |
關於 SQLite 資料庫格式
SQLite 資料庫是一連串大小固定且一致的頁面,第一頁的前 100 個位元組是描述整個檔案的標頭。其餘的一切——資料表、索引,連結構描述本身——都是由這些頁面組成的 B-tree,而結構描述不過是一張叫 sqlite_schema 的資料表,和其他資料表一樣存在第一頁。這種一致性正是這個格式如此穩定的原因:2004 年由 SQLite 3.0 寫出來的檔案,今天照樣打得開,而且專案已經承諾讓它一路可讀到 2050 年。
檔頭結構,一個位元組一個位元組看
| 位移 | 長度 | 欄位 | 意義 |
|---|---|---|---|
| 0 | 16 | Header string | ASCII 文字 'SQLite format 3' 後面接一個 NUL 位元組——剛好 16 個位元組。那個 3 是檔案格式的世代,不是程式庫版本,而且從 2004 年起就沒變過。 |
| 16 | 2 | Page size | 大端序,是你在桌面上少數會遇到的大端序格式之一。512 到 32768 之間的 2 的次方,或字面值 1 代表 65536,因為 65536 塞不進兩個位元組。現代的預設是 4096,所以這個欄位通常讀到 10 00。 |
| 18 | 1 | File format write version | 1 代表舊式的 rollback journal,2 代表 write-ahead logging。WAL 模式的資料庫需要它的 -wal 和 -shm 夥伴檔案是完整的,所以只複製一個運作中資料庫的 .db,會默默弄丟最近的交易。 |
| 19 | 1 | File format read version | 1 或 2,意義同上。大於 2 的值等於告訴舊版程式庫「這個檔案用了你不懂的東西」,它必須拒收而不是猜。 |
| 20 | 1 | Reserved space per page | 每一頁尾端刻意不使用的位元組數,通常是 0。SEE 之類的加密擴充會把它設起來,好挪出空間放每頁的 nonce 與驗證標籤,所以一個打不開的 SQLite 檔案,答案常常就在位移 20 這裡。 |
| 21 | 1 | Maximum embedded payload fraction | 必須是 64。它本來是個調校旋鈕,結果從來沒開放調整,所以出現別的值代表標頭壞了,不是什麼罕見變體。 |
| 22 | 1 | Minimum embedded payload fraction | 必須是 32。它和上下兩個位元組加起來,就是標頭提供的最便宜完整性檢查——連著讀到 64、32、32。 |
| 23 | 1 | Leaf payload fraction | 必須是 32。固定不變的理由和上面一樣:這些值本來打算做成可設定的,最後沒有。 |
| 24 | 4 | File change counter | 每次有交易修改檔案就遞增。讀取端靠它發現自己快取的頁面過期了,所以想知道某個資料庫從你上次看過之後有沒有被寫入,看它最快。 |
| 28 | 4 | Database size in pages | 標頭裡記的大小。只有在它和位移 92 的 version-valid-for 數字相符時才可信;不相符的話,真正的大小是檔案長度除以頁面大小,因為有舊版程式庫寫過這個檔案卻沒有維護這個欄位。 |
| 32 | 4 | First freelist trunk page | 未使用頁面清單起始的頁號,沒有的話是 0。刪掉的資料列會把頁面留在這張清單上,而不是把檔案縮小,這就是為什麼資料庫在跑 VACUUM 之前永遠不會變小。 |
| 36 | 4 | Freelist page count | 那張清單上有幾個頁面。資料庫不大、這個數字卻很大,代表這個檔案曾經被刪掉大量資料而且還抓著那些空間不放——也代表被刪掉的資料列在 hex 編輯器裡還救得回來。 |
| 40 | 4 | Schema cookie | 結構描述一有變動就往上加。預先編譯的敘述會記下自己編譯時的值,這個值一變就自動重新編譯,SQLite 就是這樣處理「連線還開著的時候 ALTER TABLE」。 |
| 44 | 4 | Schema format number | 1 到 4。目前是 4,從 2006 年起就是預設;DESC 索引與布林字面值都靠它。舊版程式庫遇到不認識的格式會拒收檔案,而不是硬讀錯。 |
| 48 | 4 | Default page cache size | 給頁面快取的建議值,由 PRAGMA default_cache_size 設定。純屬建議——程式庫可以不理它,而現代版本大致上真的不理。 |
| 52 | 4 | Largest root b-tree page | 只有在 auto-vacuum 或 incremental-vacuum 模式下才非零,用來記錄最大的 root page,好維護指標對應表。0 代表這個資料庫永遠不會自己縮小。 |
| 56 | 4 | Text encoding | 1 是 UTF-8、2 是 UTF-16 小端序、3 是 UTF-16 大端序。建立資料庫時設定一次,之後不可變。實際上幾乎所有東西寫的都是 UTF-8,這裡假設錯了去讀字串,整個資料庫就會變成一堆亂碼。 |
| 60 | 4 | User version | SQLite 自己完全不碰;由 PRAGMA user_version 讀寫。應用程式拿它當結構描述的遷移版本號,所以當你想搞清楚是哪一版的程式寫出這個資料庫時,它是整個檔案裡最有用的 4 個位元組之一。 |
| 64 | 4 | Incremental vacuum mode | incremental-vacuum 生效時為非零,而且只有在位移 52 的最大 root page 欄位也非零時才有意義。 |
| 68 | 4 | Application ID | 由 PRAGMA application_id 設定,讓程式可以宣告這個資料庫檔案屬於它自己的格式。只要應用程式願意設,偵測器就能說出「這是 Firefox 的 places 資料庫」,而不是只說「這是 SQLite」。 |
| 72 | 20 | Reserved for expansion | 20 個必須為零的位元組。它們零了二十年,所以這裡出現非零內容,是「有東西蓋過標頭」的可靠跡象。 |
| 92 | 4 | Version valid for number | 下面那個版本號最後一次被寫入時,變更計數器的值。它和位移 24 的變更計數器不相等,代表有舊版程式庫改過這個檔案,位移 28 那個標頭內記的大小就不能信。 |
| 96 | 4 | SQLITE_VERSION_NUMBER | 最後寫入這個檔案的程式庫版本,編碼方式是主版本乘一百萬、加次版本乘一千、再加修訂號。0x002E8DF8 是 3051000,也就是 SQLite 3.51.0——4 個位元組就告訴你最後動過這個資料庫的是哪個 build。 |
怎麼跟長得像的格式分開
| 容易搞混的 | 怎麼分 |
|---|---|
| WAL mode | 前 16 個位元組一模一樣;差別在位移 18 和 19 那一對位元組讀到的是 2 而不是 1。這件事很重要,因為最新提交的資料可能根本不在 .db 檔案裡——它在旁邊那個 -wal 檔——所以只複製資料庫會默默弄丟交易。先做 checkpoint,或是三個檔案一起複製。 |
| Encrypted SQLite | SQLCipher 之類的工具連標頭一起把整個檔案加密,所以位移 0 是一堆看起來很隨機的位元組,沒有偵測器會判它是 SQLite。SEE 擴充則走相反路線,讓標頭保持可讀,但把位移 20 的保留空間位元組設成非零。所以一個被認成 SQLite 卻打不開的檔案,值得去看位移 20。 |
| OLE2 / CFB | 兩種通用型的「一個檔案裝下整個檔案系統」。SQLite 開頭是 16 個位元組的可讀 ASCII;Compound File Binary 容器開頭是 D0 CF 11 E0 A1 B1 1A E1。兩者都用固定大小的頁面、都維護 free list、也都很怕寫到一半中斷——但 SQLite 的格式公開、有版本、每個欄位都有文件,CFB 前十五年完全不是這樣。 |
| Parquet | 每當有人問「資料要用哪一種單檔格式出貨」,就會冒出這一對。Parquet 是欄式、不可變的,頭尾都用 'PAR1' 標記;SQLite 是列式、支援交易、可以就地更新。掃十億列裡的某一欄,Parquet 完勝;需要索引、需要 join、需要把一列寫回去,SQLite 完勝。 |
常見的壞法
- -wal 或 -journal 檔案弄丟了。資料庫本體完好無缺,讀取端卻還是失敗、或默默回捲,因為複製的當下有交易正在進行。這根本不是 .db 損壞——是複製不完整——而且這是大家弄壞 SQLite 資料庫最常見的方式。
- 在寫入過程中被複製。因為一次提交會動到好幾個頁面,在交易進行到一半時把檔案快照下來,抓到的是新舊頁面的混合體,而那是任何一致狀態都不曾存在過的組合。標頭解析得很漂亮、頁數也很合理,然後 B-tree 走到一半掉進屬於另一張資料表的頁面。用 backup API 或 VACUUM INTO,不要用 cp。
- 文字模式傳輸,或是被編輯器重寫過。每個多位元組欄位都是二進位,而標頭那段 ASCII 前綴裡沒有 0D 0A,所以它毫髮無傷,後面每個頁面邊界卻全部偏移。檔案還是被認成 SQLite,而且從第一頁開始就讀不了。
- 下載被截斷。位移 28 的標頭宣告的頁數超過這個檔案裝得下的量,而 SQLite 檢查的正是這件事:把宣告的大小和「檔案長度除以位移 16 的頁面大小」比一比。缺一截的檔案可能還是能以唯讀方式打開,並提供所有頁面剛好落在截斷點以前的資料表。
- 標頭位元組被別的程式蓋掉。位移 21、22、23 這三個常數必須讀到 64、32、32,位移 72 那 20 個位元組必須是零。這是判斷標頭有沒有被踩過最便宜的測試,也抓得到「某個程式以為這個檔名是自己的、就直接寫下去」的情況。
這個格式現在還在用嗎
SQLite 是全世界部署量最大的資料庫引擎,而且不會退場:每一台 Android 與 iOS 裝置、每一個主流瀏覽器、macOS、數不清的桌面應用程式,以及大量嵌入式韌體,狀態全都存在 SQLite 檔案裡。只要資料有結構、需要查詢、而且住在單一台機器上,它就是首選——應用程式設定、快取、訊息紀錄、離線資料、本機分析——因為它給你真正的交易與真正的 SQL,不需要伺服器、不需要設定、也不需要網路。它不適合的情況是多台機器要同時寫入,因為它的鎖是以檔案為基礎的,放在網路檔案系統上就會壞掉,那條線之後就換 PostgreSQL 或 MySQL 上場。它也越來越常被當成應用程式的檔案格式而不是資料庫用:單一個 .sqlite 就裝得下整個專案,可以就地更新而不必整份重寫,而且存檔存到一半當機也活得下來——這是自訂二進位格式幾乎做不到的。考慮到這個格式有白紙黑字的穩定性保證,加上它的測試套件比程式庫本身還大,拿它來當典藏格式是很合理的賭注。
在這個站上打開 SQLite 資料庫檔
這些全部在你的瀏覽器裡跑,檔案不會離開你的電腦。
相關格式
容易和 SQLite 資料庫搞混的格式,可以比對它們的開頭位元組。
| 格式 | 開頭位元組 | 副檔名 |
|---|---|---|
| Parquet | 50 41 52 31 | .parquet |
| OLE2 / CFB | D0 CF 11 E0 A1 B1 | .doc |