最近一段時間查閱了一些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的介面使用。