sqlite淺析2-SQLITE存儲分析-SQLITE文件格式分析

 

 

1.       SQLITE存儲分析

1.1          SQLITE存儲分析

1.1.1           存儲結構介紹

 

 

SQLite 有3 類數據庫。除內存數據庫外,SQLite 把每個數據庫(main 或temp)都存儲到一個單獨的文件中。

SQLite 數據庫文件由固定大小的“頁(page)”組成。頁的類型可以是:Btree 頁、空閒(free)頁或溢出(overflow)頁。Btree 又可以是B-tree 或B+tree,每一種樹的結點又區分爲內部頁和葉子頁。一個數據庫文件中可能沒有空閒頁或溢出頁,但必然有Btree 頁。其實SQLite 還有兩種頁。一種稱爲“鎖頁(lockingpage)”。只有1 頁,位於數據庫文件偏移爲1G 開始的地方,如果文件不足1G,就沒有此頁。該頁是用於文件加鎖的區域,不能存儲數據(參源代碼io.h)。好在,如果只是讀數據,即使文件大於1G,也不會有指針指向此頁,因此下面我們就不再提它了。另一種稱爲“指針位圖頁(pointer-mappage)”,這類頁用於在auto-vacuum的數據庫中存儲元數據。

 

一個SQLite 數據庫文件由多個多重Btree 構成。每個Btree 存儲一個表的數據或一個表的索引,索引採用B-tree,而表數據採用B+tree,每個Btree 佔用至少一個完整的頁,每個頁是Btree 的一個結點。每個表或索引的第1 個頁稱爲根頁,所有表或索引的根頁編號都存儲在系統表sqlite_master 中,表sqlite_master 的根頁爲page 1。

 

///////////////////////////////////////////////////////////////////////////////////////////////////

 

B-tree結構,

///////////////////////////////////////////////////////////////////////////////////////////////////

** Each btree pages is divided intothree sections:  The header, the

** cell pointer array, and the cellcontent area.  Page 1 also has a 100-byte

** file header that occurs before thepage header.

**

**     |----------------|

**     | file header    |   100 bytes. Page 1 only.

**     |----------------|

**     | page header    |   8 bytes for leaves.  12 bytes for interior nodes

**     |----------------|

**     | cell pointer   |   |  2bytes per cell.  Sorted order.

**     | array          |   | Grows downward

**     |                |   v

**     |----------------|

**     | unallocated    |

**     | space          |

**     |----------------|   ^  Grows upwards

**     | cell content   |   | Arbitrary order interspersed with freeblocks.

**     | area           |   |  andfree space fragments.

**     |----------------|

 

 

Btree 頁內部以 (cell)爲單位來組織數據,一個cell包含一個(或

部分,當使用溢出頁時)payload(也稱爲Btree 記錄)。由於各類數據大小各不相同,cell

的大小也就是可變的,所以Btree 頁內部的空間需要進行動態分配(程序內部動態分配,不是動態申請空間),cell是Btree 頁內部進行空間分配和回收的基本單位。

 

頁內所有cell的內容集中在頁的底部,稱爲“cell內容區”,由下向上增長。由於cell的大

小可變,因此需要對每個cell在頁內的起始位置(稱爲cell指針數組)進行記錄。cell指針保存在cell指針數組中,位於頁頭之後。cell指針數組包含0 個或多個指針,由上向下增長。

 

cell指針數組和cell內容區相向增長,中間部分爲未分配空間。系統儘量保證未分配空間位

於最後的指針之後,這樣,就很容易增加新的cell,而不需要整理碎片。

cell不需要是相鄰和有序的,但cell指針是相鄰和有序的。每個指針佔2 個字節,表示該單

元在單元內容區中距頁開始處的偏移。頁中cell的數量保存在頁頭中。

 

文件頭格式

 

頁頭格式

頁頭包含用來管理頁的信息,它通常位於頁的開始處。對於數據庫文件的page 1,頁頭始於

第100 個字節處,因爲前100 個字節是文件頭(file header)。

 

頁類型標誌:

如果leaf 位被設置,則該頁是一個葉子頁,沒有兒子;

如果zerodata 位被設置,則該頁只有關鍵字,而沒有數據;

如果intkey 位被設置,則關鍵字是整型;

如果leafdata 位設置,則tree 只存儲數據在葉子頁。

第1 個空閒塊的偏移量:

由於隨機地插入和刪除單元,將會導致一個頁上單元和空閒區域互相交錯。cell內容區域中

沒有使用的空間收集起來形成一個空閒塊鏈表,這些空閒塊按照它們地址的升序排列。頁頭

偏移爲1 的2 個字節指向空閒塊鏈表的頭。每個空閒塊至少4 個字節,因爲一個空閒塊的開

始4 個字節存儲控制信息:前2 個字節指向下一個空閒塊(0 意味着沒有下一個空閒塊了),

後2 個字節爲該空閒塊的大小。

 

cell內容區的起始地址:

cell內容區的起始地址記錄在頁頭偏移爲5 的地方。這個值爲cell內容區域和未使用區域的

分界線。

 

cell內容區碎片字節總數, offset 7。

由於空閒塊大小至少爲4 個字節,所以cell內容區中的3 個字節或更小的空閒空間(稱爲碎

片,fragment)不能存在於空閒塊列表中。所有碎片的總的字節數將記錄在頁頭偏移爲7 的位置(碎片最多爲255 個字節,在它達到最大值之前,頁會被整理)。

 

最右兒子的頁號:

如果本B-tree 頁是葉子頁,則無此域,頁頭長爲8 個字節。如果本B-tree 頁爲內部頁,則有

此域,頁頭長爲12 個字節。頁頭偏移爲8 的4 個字節包含指向最右兒子的指針,該指針的

含義將在第2 章介紹。

 

cell內容區空閒空間:被一個鏈表鏈接,每個數據塊最少四個字節,鏈表塊數據結構

 

 

Cell內容區:

 

cell是變長的字節串。一個單元包含一個(或部分,當使用溢出頁時)payload。

大小 說明

4  left節點的頁碼,葉子節點無此字段。

var(1–9) Payload 大小,以字節爲單位。

var(1–9) 數據庫記錄的Rowid 值。

* Payload 內容,存儲數據庫中某個表一條記錄的數據。

4 溢出頁鏈表中第1 個溢出頁的頁號。如果沒有溢出頁,無此域。

 

這裏有個知識點很重要,如果不清楚,就分析不了對應的raw data,就是number是用可變長整數表示的,如何知道number佔幾個字節,如何知道它實際的大小,如果沒有注意分析代碼裏的註釋和實際的數據,是意識不到這一點的。可變長整數的解釋如下,具體的實例後續會有分析。

 

 

溢出頁:


(cell)具有可變的大小,而頁的大小是固定的,這就有可能一個單元比一個

完整的頁還大,這樣的單元就會溢出到由溢出頁組成的鏈表上,如下圖所示:



溢出頁號爲0 時表示此頁爲溢出頁鏈表的表尾頁。

 

 

空閒頁:


空閒頁有兩種類型:trunk page(主幹頁)和leaf page(葉子頁)。

文件頭偏移爲32 處的指針指向空閒鏈表的第一個trunk page,每個trunk page 指向多個葉子頁。

文件頭偏移36 處的4 個字節爲空閒頁的總數量,包括所有的trunk page 和leaf page。

 

空閒頁鏈表的結構如下圖所示:


其中,trunk page 的格式(從頁的起始處開始)如下:

(1)4 個字節,指向下一個trunk page 的頁號,0 表示鏈表結束;

(2)4 個字節,該頁leaf page 的數量;

(3)0 個或多個指向leafpage 的頁號,每項4 個字節。

 

SQLite 對leafpage 的格式沒有規定。

 

 

指針圖頁:

只有auto-vacuum 數據庫纔有指針圖頁(Pointer map page)。如果數據庫文件頭偏移爲52 字節的地方爲一非零值,該數據庫爲auto-vacuum數據庫。

數據庫中所有的指針圖頁共同構成一個查找表,利用該表可以確定數據庫中各頁的類型及其

父親頁的頁號。查找表將頁按下表分類:

 

指針圖頁本身不出現在指針圖查找表中。Page 1 也不出現在指針圖查找表中。指針圖入口格

式如下圖所示:

 

 

 

每個指針圖查找表入口使用5 個字節。第1 個字節爲頁類型,後4 個字節爲父親頁號(高位

字節在前)。每個指針圖頁可以包含

num-entries := usable-size / 5

個入口,其中“可用大小”爲頁大小減頁尾部保留空間的大小(參1.2 節)。

如果數據庫是auto-vacuum 的,page 2 永遠是指針圖頁。它保存了從第3 頁到第(2 +

num-entries)頁的指針圖查找表入口。其中page 2 的頭5 個字節保存的是第3 頁的指針圖查

找表入口,5~9 字節保存的是第4 頁的指針圖查找表入口,依此類推。

數據庫中下一個指針圖頁的頁號是(3 +num-entries),它保存了從第(4 + num-entries)頁到第

(3+2*num-entries)頁的指針圖查找表入口。一般而言,對於任何大於0 的n,頁號爲(2 + n*

num-entries)的頁爲指針圖頁。非auto-vacuum數據庫沒有指針圖頁。

 


 

1.1.1  實踐分析

http://www.sqlite.org/download.html下載sqlite-tools-win32-x86-3170000.zip工具包,調出DOS控制檯,

 

創建數據庫:

C:\zdoc\doc_shah\sqlite\sqlite-tools-win32-x86-3170000>sqlite3student.db

SQLite version 3.17.02017-02-13 16:02:40

Enter ".help" forusage hints.

 

創建表:

CREATE TABLE basic(

id intefer primady key,

name text,

sex text,

age integer,

class text,

number integer,

phone text,

address text);

 

 

插入數據:

INSERTINTO "basic" VALUES(1,'Bagels','M',18,'3-3',980303006,'13246789876','guangdong shenzhen');

 

INSERTINTO "basic" VALUES(1,'Peter','M',18,'3-3',980303008,'13940709836','shandong jinan');

 

INSERTINTO "basic" VALUES(1,'Robot','M',16,'3-3',980303004,'13646709872','guangdong heyuan');

 

INSERTINTO "basic" VALUES(1,'Alice','F',17,'3-3',980303026,'13246357876','hunan changsha');

 

 

可以看到生成了一個8K的.db文件,根據後面的分析我們知道,這個db文件有兩個page,每個page爲4K。

 

///////////////////////////////////////////////////////////////////////////////////////////////////

 

可以用sqlitespy打開.db文件,看到我們建立的數據庫和表。



還可以用其他編輯工具打開.db文件,以二進制的方式查看文件。

 

 

1.1          表數據頁

 

表數據用B+tree 來存儲。

1.1.1  Page1

 

File header文件頭從db文件0位置開始,100 個字節




前16 個字節爲頭字符串,程序中固定設爲"SQLite format 3"。

0X1000:頁大小,0X1000=4096 字節。

0X01:文件格式版本(寫),值爲1。

0X01:文件格式版本(讀),值爲1。

0X40:Btree 內部頁中一個cell最多能夠使用的空間。0X40=64,即25%。

0X20:Btree 內部頁中一個cell使用空間的最小值。0X20=32,即12.5%。

0X20:Btree 葉子頁中一個cell使用空間的最小值。0X20=32,即12.5%。

0X00000005:文件修改計數,現在已經修改了5 次,分別是1 次創建表和4次插入記錄。

從0X20 開始的4 個字節:空閒頁鏈表首指針。當前值爲0,表示該鏈表爲空。

從0X24 開始的4 個字節:文件內空閒頁的數量。當前值爲0。

從0X28 開始的4 個字節:Schema version。當前值爲0X00000001。以後,每次sqlite_master

表被修改時,此值+1。

從0X38 開始的4 個字節:採用的字符編碼。此處爲0X00000001,表示採用的是UTF-8 編

碼。

注意:在SQLite 文件中,所有的整數都採用大端格式,即高位字節在前。


 

 

Page header頁頭從0X64=100 處開始,8個字節(葉子頁8字節,內部頁12字節)。




說明:

0X0D:說明該頁爲B+tree 的葉子結點。可以看出,如果是B+tree

的葉子頁,該字節值爲0X0D,如果是B+tree 的內部頁,該字節值爲0X05,如果是B-tree

的葉子頁,該字節值爲0X0A,如果是B-tree 的內部頁,該字節值爲0X02。

0X0000:第1 個空閒塊的偏移量。值爲0,說明當前空閒塊鏈表爲空。

0X0001:本頁的cell數。當前系統表中只有一條記錄,所以本頁當前只有1 個cell。

0X0F63:cell內容區的起始地址。

0X00:碎片的字節數。當前值爲0。



 

 

Cell內容區分析,根據page header的offset,找到cell內容區的地址爲0X0F63。



這是個葉子節點,所以沒有left child的page number,datanumber是可變長整形,爲0x81 1a,實際數值爲0x009a,就是154,0X0F62+0x009a=0x0ffc,加上這兩個var字段,一個爲2,一個爲1,地址就是0x0fff,剛好到達頁尾。

0X01:(table 對象)在sqlite_master 表中對應記錄的rowid,值爲0X01。

 

 

 

 

每個payload 由兩部分組成。第1 部分是記錄頭,由N+1 個可變長整數組成,N 爲記錄中的

字段數。第1 個可變長整數(header-size)的值爲記錄頭的字節數。跟着的N 個可變長整數與

記錄的各字段一一對應,表示各字段的數據類型和寬度。用可變長整數表示各字段類型和寬

度的規定如下表所示:

 

 

header-size 的值包括header-size 本身的字節和Type1~TypeN 的字節。

Data1~DataN爲各字段數據,與Type1~TypeN一一對應,類型和寬度由Type1~TypeN指定。

本例的payload 數據爲:

0X07:記錄頭包括7 個字節。

0X17:字段1。TEXT,長度爲:(23-13)/2=5。值爲:table。

0X17:字段2。TEXT,長度爲:(23-13)/2=5。值爲:basic。

0X17:字段3。TEXT,長度爲:(23-13)/2=5。值爲:basic。

0X01:字段4。整數,長度爲1。值爲:0X02。表示本表B+tree 的根頁編號爲2。

0X8213:字段5。TEXT。0X8213 爲可變長整數,轉換爲定長爲0X113=275。可知字段長度

爲:(275-13)/2=131=0X83。就是create語句的部分。




 

 

1.1.1  Page2

 

這裏首先就是page header,只有一個結點,既是根頁,也是葉子頁。

 

說明:

0X0D:說明該頁爲B+tree 的葉子結點。可以看出,如果是B+tree

的葉子頁,該字節值爲0X0D,如果是B+tree 的內部頁,該字節值爲0X05,如果是B-tree

的葉子頁,該字節值爲0X0A,如果是B-tree 的內部頁,該字節值爲0X02。

0X0000:第1 個空閒塊的偏移量。值爲0,說明當前空閒塊鏈表爲空。

0X0004:本頁的cell數。當前表中有4條記錄,所以本頁當前有4 個cell。

0X0F31:cell內容區的起始地址。

0X00:碎片的字節數。當前值爲0。

 

 

Cell指針數組:存在4個cell的指針,因爲是反向生長,所以第一個cell的地址偏移最大,按順序排列。注意雖然在pageheader部分,cell內容區的起始地址是0X0F31,但本表的第1 條記錄地址是0X0FC9.

 

Payload的分析也如同page1.


 

 

1.1          索引B-tree

1.1.1  Index Page

索引頁數據結構爲B-tree,有內部頁和葉子頁。B-tree 中只存儲關鍵字段的值和對應記錄的rowid 值。

可以使用如下語句生成一個索引頁,

CREATE INDEX basic_number_idx onbasic(number COLLATE NOCASE);

之後db文件大小增加4k,爲一個頁面(頁面數依賴記錄的數目)。

 

說明:

0X0a:說明該頁爲B-tree 的葉子頁,內部頁0X02。

0X0000:第1 個自由塊的偏移量。0,說明當前自由塊鏈表爲空。

0X0004:本頁有4 個cell。

0X0fdd:cell內容區的起始位置。

0X00:碎片的字節數,當前爲0。

 

葉子頁無子節點,故字段無。

 

cell指針數組在頁頭之後,有4 個指針。

 

cell內容區的數據如下:



最後的一個cell,即0X0fdd對應的數據解釋如下:

0X08:Payload 數據的字節數。

0X03:記錄頭包括3 個字節,含自己。

0X04:字段1。TEXT,長度爲:4。字段值爲:0X3A6E3CB2,即number列的值98030326。

0X01:字段2。整數,長度爲1。字段值爲:0X04,表示索引值所對應記錄的關鍵值(即記錄的rowid 值)爲4。

 

其他的解釋類似,需要注意幾點,1是索引表的cell記錄是按順序排列的;2是第一行的rowid在數據裏面是忽略的,所以只要7個字節;3是習慣這種payload格式的表示方法,通過pageheader和cell pointer array找到每個cell的起始地址,然後每段字節數據的解析和對對應關係。






如果覺得我的文章對您有用,請打賞。您的支持是對我莫大的認可



發佈了96 篇原創文章 · 獲贊 40 · 訪問量 29萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章