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的起始地址,然後每段字節數據的解析和對對應關係。
如果覺得我的文章對您有用,請打賞。您的支持是對我莫大的認可!