最近一段時間查閱了一些SQLite3數據庫的文件結構,在此集中記錄一下。
首先,先介紹一個SQLite3數據庫,我能夠直觀感受到的是單一磁盤文件,就是說SQLite數據庫被存在文件系統的單一磁盤文件內(如果有日志文件的話,數據庫映像就將存在兩個文件中),只要有權限就可以隨意的訪問和拷貝,而其他的數據庫引擎,基本都會將數據庫存放在一個磁盤目錄下,然后由該目錄下的一組文件構成該數據庫的數據文件。(以上參考《SQLite 學習手冊》)。
下面進入正題,來講講SQLite3數據庫文件結構。
SQLite3數據庫總體結構
SQLite3數據庫表的B+樹結構
SQLite數據庫頁面結構
注:以上三張圖來自朱清華的論文《基於Android手機SQLite的取證系統設計實現》
SQLite3數據庫頭結構
用winhex打開一個數據庫文件(數據庫名.db),前100個字節就是數據庫的頭結構,這是固定的。如下圖:
這100個字節所代表的含義是:
起始地址 | 終止地址 | 含義 | 備注 |
0 | 15 | 頭字符串 | 一般都是SQLite format 3 |
16 | 17 | 頁大小 | 表示數據庫的頁大小,上圖為0x1000,也就是4096個字節 |
18 | 18 | 文件格式版本寫 | 一般是0x01 |
19 | 19 | 文件格式版本讀 | 也是0x01 |
20 | 20 | 每頁尾部保留空間大小 | 默認是0 |
21 | 21 | btree內部頁單元最多能用的空間 | 0x40,也就是25% |
22 | 22 | btree內部頁單元最少使用空間 | 0x20,也就是12.5% |
23 | 23 | btree葉子頁單元最少使用空間 | 0x20,12.5% |
24 | 27 | 文件修改次數 | 該值由事務增加 |
28 | 31 | 數據庫占據的總頁數 | |
32 | 35 | 空閑頁鏈表頭指針 | 我覺得對於在auto-vacuum數據庫, 由於空閑頁只要出現一個空閑頁就將這個空間歸還給操作系統,因此,他的空閑頁鏈表頭指針一直為0,我查看了一個開啟auto-vacuum選項的數據庫發現這個值都為0 |
36 | 39 | 空閑頁數量 | 這個也同樣都為0 |
40 | 44 | schema版本號 | |
44 | 47 | 值為1-4之間 | |
48 | 51 | 默認頁緩存大小 | |
52 | 55 | b-tree最大根頁號 | 當創建數據庫的時候啟動auto-vacuum功能時,此處的值表示b-tree最大的根頁號,沒有啟用該功能時,此處值為0 |
56 | 59 | 編碼方式 | 1對應utf-8,2對應utf-16le,3對應utf-16be |
60 | 63 | 用戶版本號 | 此處的值由用戶使用pragma讀取或設置 |
64 | 67 | 是否啟用incremental-vacuum | 對於auto-vacuum數據庫,當為incremental-vacuum時為1 |
68 | 71 | 用戶應用程序ID | 由pragma application_id設置的應用ID |
72 | 91 | 保留空間 | 為擴展空間預留 |
92 | 95 | 有效版本 | |
96 | 99 | SQLite數據庫版本號 |
SQLite3數據庫頁頭結構
數據庫頭信息存儲在根頁中,緊接着數據庫頭的是頁頭信息,頁頭可以是中間頁頁可以是葉子頁,當數據庫很小的時候,數據庫頭結束后就是頁頭,也就是說根頁就是葉子頁。
上圖中藍色部分就是一個頁頭信息,頁頭的結構如下:
起始地址 | 結束地址 | 含義 | 備注 |
0 | 0 | 該頁類型 | 0x0d表示b+tree葉子頁,0x05是b+tree內部頁,0x0a是b-tree葉子頁,0x02是b-tree內部頁 |
1 | 2 | 第一個自由塊的偏移量 | 指的是第一個自由塊的偏移地址,這個地址是相對於該頁的頁首而言的,因此在數據庫中查找的時候需要換算成絕對偏移量,也就是加上該頁頁頭的偏移量 |
3 | 4 | 本頁單元數 | |
5 | 6 | 單元內容起始地址 | 這個起始地址是數據庫存儲的最新的那條記錄的地址 |
7 | 7 | 空閑塊數 | 指的是空閑塊大小小於3個字節的數目 |
8 | 11 | 最右孩子頁號 | 只有內部頁有這一屬性,葉子頁是沒有的 |
12 | 12+本頁單元數*2 | 該頁中每個單元的起始地址 | 這個地址也是一個相對量,實際使用的時候需要換算 |
將上面的表格和頁頭信息相對應可以發現,數據庫頭結束后緊跟的是一個b+tree內部頁頁頭,該頁沒有自由塊,共有5個單元,單元內容的起始地址是0x0FE7,空閑塊數為0。最右孩子頁號為0x00001B,接下來10個字節分別代表該頁五個單元的起始偏移。在0x0FE7后面可以看到還有其他的信息,這些信息就是被部分覆蓋前的原始信息。
接下來看一個b+tree葉子頁的詳細情況:
可以發現上面該頁的頁頭標志是0x0D,也就是葉子頁,並且該頁有被刪除的數據,他的第一個自由塊的起始地址是0x0729,該頁有14個單元,單元內容起始地址是0x0122,空閑塊數是0,該頁每一最右孩子頁號這一個區域。
SQLite3數據庫Sql_master表結構
同樣的Sqli_master表頁存儲在根頁中。Sql_master表是系統表,它里面存儲着各個表的建表SQL語句,下面是Sql_master表的詳細結構:
type | name | tal_name | rootpage | sql |
text字段,系統表的創建類型,有table,index,trigger,view四種類型 | text字段,表、索引、觸發器、視圖的名字 | 對表和視圖來說,這個值和name字段一致,對索引和觸發器來說是建立在那個表上的表名字 | 對表和索引來說是根頁的頁號,至今看到的根頁號都是用一個字節表示的 | text字段,創建表、索引、觸發器或視圖所使用的sql語句 |
接下來我們看一個具體的單元內容以及一個單元刪除前后的頁頭以及單元頭記錄頭的變化。
SQLite3數據庫單元結構
記錄大小 | RowID | Payload | overflow |
該字段是一個1到9個字節的變長整數,也就是記錄內容部分的大小 |
該單元的記錄在表中的行ID數,同樣用1-9個字節的變長整數表示 | 記錄內容部分 | 溢出頁鏈表的第一個指針,沒有溢出頁的時候就沒有這個域,給字段為4個字節 |
SQLite3記錄內容部分的結構是:
記錄頭 | type | Payload |
一個可變長整數,表示記錄頭加上type的個數 | 記錄中每一個字段的類型描述,描述本身包含了字段的類型和長度(字節數) | 記錄真正的內容部分 |
type的類型具體有一下幾種,計算字段的長度方法也附在表中:
type的值 | 表示的字段類型 | 長度(占據字節數) |
0 | NULL | 0 |
X(X∈{1,2,3,4}) | 有符號整數 | X |
5 | 有符號整數 | 6 |
6 | 有符號整數 | 8 |
7 | IEEE float | 8 |
8 | 有符號整數 | 0 |
9 | 有符號整數 | 0 |
X(X∈{X>12,且X%2==0}) | BLOB | (X-12)/2 |
X(X∈{X>13,且X%2==1}) | TEXT | (X-13)/2 |
SQLite3數據庫單眼數據刪除前后變化情況
下面看一個具體的單元在刪除前后的對比情況:
短信數據庫的一條短信被刪除前的單元數據信息為:
可以看到該單元的記錄內容大小為0x36,也就是54個字節,該單元的記錄在短信表的第5行,記錄頭的大小為0x1c,也就是28個字節,也就是說type的字節數為27個字節,第一個type值為0x00,也就是0;第二個type類型為0x02,是一個占據兩個字節的有符號整數,與該符號相對應的值為0x06C2,下一個type值為0x17,是大於13的奇數,因此他表示一個長度為(23-13)/2=5的text文本,該字段對應的值為10086;一次類推,可以將整個字段所代表的含義解析出來,可以解析出來短信的內容為:text3。
下面來看一看刪除該短信后,該單元以及單元所在頁的頁頭改變:
與上面的數據進行對比我們可以發現,在該單元變成自由塊之后只有前四個字節發生了變化,這是變化最少的情況,因為該數據比較少,單元頭、RowID、記錄頭的大小都只用了一個字節就可以表示。
改變的前四個字節所代表的含義是:
前兩個字節表示下一個自由塊的起始地址;
后兩個字節表示該自由塊的大小,可以發現自由塊的大小值比刪除前單元的記錄內容大小值多了2,也就是說自由塊的大小值記錄的是刪除前整個單元的大小。
下面看看頁頭的變化:
刪除前該頁頁頭的情況是:
刪除后該頁的情況是:
可以看到刪除后的第一個自由塊起始地址不在是0,而是被刪除的那天短信所在單元的起始地址0X09E7,該頁的單元數從原來的7個變成了5個(變成5個是因為我在刪除過程中手誤刪除了兩條短信)。除此之外我們也能夠發現,在記錄該頁各單元起始地址的區域結束后,后面的位置存儲的數據是一致的,這也是一種部分覆蓋的情況。
變長型整數
SQLite數據庫中有很多的整數是設定為變長整數類型的,在這里也記錄一下變長整數的規則。
變長整數是8個bit為一組(也就是一個字節),最高位是判斷為,當最高位為0時表示當前字節為該整數的最后一個字節,當最高位為1時表示后面的一個字節也表示這個整數。用一個例子來說明:
eg:0x81 95 E3 21
上面的整數第一個字節的最高位為1,繼續向后讀一個字節;第二個字節最高位也為1,繼續向后讀一個字節,第三個字節最高位也為1,繼續向后讀一個字節;第四個字節最高位為0,停止。
接下來就是將變長整數轉為定長整數,轉化的過程就是去符號位的過程。
首先將16進制的變長整數轉為2進制:
1000 0001 1001 0101 1110 0011 0010 0001
去掉符號位后變成:
000 0001 001 0101 110 0011 010 0001
從低位像高位四個一組重新組成16進制數:
0x25 71 A1
從變長轉定長的過程就結束了。
結束語
這篇博客參考了很多資料,主要的參考資料有:
1.《SQLite數據庫文件格式全面分析》——空轉,這篇文章對SQLite3數據庫文件結構分析的很詳細,但是其中有一個有歧義的地方是,作者在2.3節介紹大文件內部頁單元結構的時候忽略了最右孩子頁號后面結根的是該頁各個單元起始地址這一情況,將各單元起始地址也歸為未分配空間部分了。
2.《基於Android手機SQLite的取證系統設計實現》——朱清華
3.http://www.runoob.com/sqlite/sqlite-java.html 該鏈接處講了SQLite數據庫的基本應用知識,包括與C/C++和Java的接口使用。