最近一段時間查閱了一些SQLite3數據庫的文件結構,在此集中記錄一下。html
首先,先介紹一個SQLite3數據庫,我可以直觀感覺到的是單一磁盤文件,就是說SQLite數據庫被存在文件系統的單一磁盤文件內(若是有日誌文件的話,數據庫映像就將存在兩個文件中),只要有權限就能夠隨意的訪問和拷貝,而其餘的數據庫引擎,基本都會將數據庫存放在一個磁盤目錄下,而後由該目錄下的一組文件構成該數據庫的數據文件。(以上參考《SQLite 學習手冊》)。java
下面進入正題,來說講SQLite3數據庫文件結構。sql
SQLite3數據庫整體結構數據庫
SQLite3數據庫表的B+樹結構緩存
SQLite數據庫頁面結構app
注:以上三張圖來自朱清華的論文《基於Android手機SQLite的取證系統設計實現》學習
SQLite3數據庫頭結構編碼
用winhex打開一個數據庫文件(數據庫名.db),前100個字節就是數據庫的頭結構,這是固定的。以下圖: spa
這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的接口使用。