前言
SQLite作為嵌入式數據庫,通常針對的應用的數據量相對於DBMS的數據量小。所以它的存儲模型設計得非常簡單,總的來說,SQLite把一個數據文件分成若干大小相等的頁面,然后以B樹的形式來組織這些頁面。而對於大型的數據庫管理系統,比如Oracle,或者DM ,存儲模型要復雜得多。就拿Oracle來說吧,它對數據文件不僅從物理上進行分塊,而且從邏輯上進行分段,盤區和頁的一個層次划分DM也一樣。不管怎么說,數據庫文件要存儲大量的數據,為了更好管理,查詢和操作數據文件,DBMS不得不從物理上、邏輯上對數據文件的數據進行復雜的組織。
1.文件格式
1.1、數據庫名稱
應用程序通過sqlite3_open(API)來打開數據庫,該函數的一個參數為數據庫文件的名稱。SQLite內部命名為main數據庫(除了臨時數據庫和內存數據庫)。SQLite對每一個數據庫都創建一個獨立的文件。
在SQLite內部,數據文件名不是數據庫名。SQLite對應用程序的每一個連接都維護着一個單獨的臨時數據庫(temp數據庫),臨時數據庫存臨時對象,例如表以及相應的索引。這些臨時對象僅僅對同一個連接可見(對同一個線程、進程的其它連接是不可見的),SQLite存儲臨時數據庫到一個單獨的臨時文件中,當應用程序關閉對main數據庫的連接時,就刪除臨時文件。
1.2、數據庫文件結構
除了內存數據庫,SQLite把一個數據庫(main和temp)都存儲到一個單獨的文件。
1.2.1、頁面(page)
為了更好的管理和讀/寫數據庫,SQLite把一個數據庫(包括內存數據庫)分成一個個固定大小的頁面。頁面大小的范圍從512-32768(兩者都包含),頁面默認大小為1024個字節(1KB),實際上頁面的上限由2個字節的有符號整數決定。整個數據庫可以看成這些頁面的數組,頁面數組的下標為頁面的編號(page number),page number從1開始,一直到2、147、483、647 (2^31– 1)。實際上,數組上界還受文件系統允許的最大文件大小決定。0號頁面視為空頁面(NULL page),物理上不存在,1號頁面從文件的0偏移處開始,一個頁面接着下一個頁面。
注意:一旦數據庫創建,SQLite使用編譯時確定的默認的頁面大小。當然,在創建第一個表之前,可以通過pragma命令改變頁面大小。SQLite把該值作為元數據的一部分存儲在文件中。
1.2.2、頁面類型
頁面(page)分四種類型:葉子頁面(leaf),內部頁面(internal),溢出頁面(overflow)和空閑頁面(free)。內部頁面包含查詢時的導航信息,葉子頁面存儲數據,例如元組。如果一個元組的數據太大,一個頁面容納不下,則一些數據存儲在B樹的頁面中,余下的存儲在溢出頁面中。
1.2.3、文件頭(file header)
作為文件開始的1號頁面比較特殊,它包括100個字節的文件頭。當SQLite創建文件時先初始化文件頭,文件頭的格式如下:
Structure of database file header |
||
Offset |
Size |
Description |
0 |
16 |
Header string |
16 |
2 |
Page size in bytes |
18 |
1 |
File format write version |
19 |
1 |
File format read version |
20 |
1 |
Bytes reserved at the end of each page |
21 |
1 |
Max embedded payload fraction |
22 |
1 |
Min embedded payload fraction |
23 |
1 |
Min leaf payload fraction |
24 |
4 |
File change counter |
28 |
4 |
Reserved for future use |
32 |
4 |
First freelist page |
36 |
4 |
Number of freelist pages |
40 |
60 |
15 4-byte meta values |
示例數據(100個字節):
53 51 4C 69 74 65 20 66 6F 72 6D 61 74 20 33 00 ; SQLite format 3. 04 00 01 01 00 40 20 20 00 00 00 11 00 00 00 00 ; .....@........ 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 01 ; ................ 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 ; ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; ................ 00 00 00 00 |
前16個字節-Header string(頭字符串):"SQLite format 3."
0x04 00 :頁面大小-Page size,即1024
0x01 01 :文件格式-File format(寫、讀各一字節),在當前的版本都為1。
0x00 : 保留空間-Reserved space,1個字節,SQLite在每個頁面的末尾都會保留一定的空間,留作它用,默認為0。
0x40 20 20 :max embedded payload fraction(偏移21)的值限定了B樹內節點(頁面)中一個元組(記錄,單元)最多能夠使用的空間,255意味着100%,默認值為0x40,即64(25%),這保證了一個結點(頁面)至少有4個單元。如果一個單元的負載(payload,即數據量)超過最大值,則溢出的數據保存到溢出的頁面,一旦SQLite分配了一個溢出頁面,它會盡可能多的移動數據到溢出頁面;下限為min embedded payload fraction value(偏移為22),默認的值為32,即12.5% ;min leaf payload fraction的含義與min embedded payload fraction類似,只不過是它是針對B樹的葉子結點,默認值為32,即12.5%,葉子結點最大的負載為通常是100%,這不用保存。
0x00 00 00 11 :文件修改計數-File change counter,通常被事務使用,它由事務增加其值。該值的主要目的是數據庫改變時,pager避免對緩存進行刷盤。
空閑頁面鏈表(Freelist):在文件頭偏移32的4個字節記錄着空閑頁面鏈的第一個頁面。
空閑頁面的數量:偏移36處的4個字節為空閑頁面的數量。
空閑頁面鏈表的組織形式如下:
空閑頁面分為兩種頁面:trunk pages(主頁面)和leaf pages(葉子頁面)。文件頭的指針指向空閑鏈表的第一個trunk page,每個trunk page指向多個葉子頁面。
Trunk page的格式如下,從頁面的起始處開始:
(1)4個字節,指向下一個trunk page的頁面號;
(2)4個字節,該頁面的葉子頁面指針的數量;
(3)指向葉子頁面的頁面號,每項4個字節。
當一個頁面不再使用時,SQLite把它加入空閑頁面鏈表,並不從本地文件系統中釋放掉。當添加新的數據到數據庫時,SQLite就從空閑鏈表上取出空閑頁面用來再存儲數據。當空閑鏈表為空時,SQLite就通過本地文件系統增加新的頁面,添加到數據庫文件的末尾。
注:可以通過vacuum命令刪除空閑鏈表,該命令通過把數據庫中數據拷貝到臨時文件,然后在事務的保護下,用臨時文件中的復本覆蓋原數據庫文件。
元數據變量(Meta variables):從偏移為40開始,為15個4字節的元數據變量,這些元數據主要與B樹和VM有關。如下:
** Meta values are as follows: ** meta[0] Schema cookie. Changes with each schema change. ** meta[1] File format of schema layer. ** meta[2] Size of the page cache. ** meta[3] Use freelist if 0. Autovacuum if greater than zero. ** meta[4] Db text encoding. 1:UTF-8 2:UTF-16LE 3:UTF-16BE ** meta[5] The user cookie. Used by the application. ** meta[6] ** meta[7] ** meta[8] ** meta[9] |
1.2.4、讀取文件頭
當應用程序調用sqlite3_open(API)打開數據庫文件時,SQLite就會讀取文件頭進行數據庫的初始化。
int sqlite3BtreeOpen( const char *zFilename, /* Name of the file containing the BTree database */ sqlite3 *pSqlite, /* Associated database handle */ Btree **ppBtree, /* Pointer to new Btree object written here */ int flags /* Options */ ){ //讀取文件頭 sqlite3pager_read_fileheader(pBt->pPager, sizeof(zDbHeader), zDbHeader); //設置頁面大小 pBt->pageSize = get2byte(&zDbHeader[16]); //… }
2.頁面結構(page structure)
數據庫文件分成固定大小的頁面。SQLite通過B+tree模型來管理所有的頁面。頁面(page)分三種類型:tree page、overflow page、free page。
2.1、Tree page structure
每個tree page分成許多單元(cell),一個單元包含一個(或部分)payload。Cell是tree page進行分配或回收的基本單位。
一個tree page分成四個部分:
(1)The page header
(2)The cell content area
(3)The cell pointer array
(4)Unallocated space
Cell指針數組與cell content相向增長。一個page header僅包含用來管理頁面的信息,它通常位於頁面的開始處(但是對於數據庫文件的第一個頁面,它開始於第100個字節處,前100個字節包含文件頭信息(file header))。
Structure of tree page header:
Offset |
Size |
Description |
0 |
1 |
Flags. 1: intkey, 2: zerodata, 4: leafdata, 8: leaf |
1 |
2 |
Byte offset to the first free block |
3 |
2 |
Number of cells on this page |
5 |
2 |
First byte of the cell content area |
7 |
1 |
Number of fragmented free bytes |
8 |
4 |
Right child (the Ptr(n) value). Omitted on leaves. |
Flag定義頁面的格式:如果leaf位被設置,則該頁面是一個葉子節點,沒有孩子;如果zerodata位被設置,則該頁面只有關鍵字,而沒有數據;如果intkey位設置,則關鍵字是整型;如果leafdata位設置,則tree只存儲數據在葉子節點。另外,對於內部頁面(internal page),header在第8個字節處包含指向最右邊子節點的指針。
Cell位於頁面的高端,而cell 指針數組位於頁面的page header之后,cell指針數組包含0個或者多個的指針。每個指針占2個字節,表示在cell content區域的cell距頁面開始處的偏移。頁面Cell單元的數量位於偏移3處。
由於隨機的插入和刪除單元,將會導致一個頁面上Cell和空閑區域互相交錯。Cell內容區域(cell content area)中沒有使用的空間收集起來形成一個空閑塊鏈表,這些空閑塊按照它們地址的升序排列。頁面頭1偏移處的2個字節指向空閑塊鏈表的頭。每一個空閑塊至少4個字節,每個空閑塊的開始4個字節存儲控制信息:頭2個字節指向下一個空閑塊(0意味着沒有下一個空閑塊了),剩余的2個字節為該空閑塊的大小。由於空閑塊至少為4個字節大小,所以單元內容空間中的3個字節或更小的空間(叫做fragment)不能存在於空閑塊列表中。所有碎片(fragment)的總的字節數將記錄在頁面頭偏移為7的位置(所以太碎片最多為255個字節,在它達到最大值之前,頁面會被整理)。單元內容區域的第一個字節記錄在頁面頭偏移為5的地方。這個值為單元內容區域和未使用區域的分界線。
2.2、單元格式(Structure of a cell)
單元是變長的字節串。一個單元存儲一個負載(payload),它的結構如下:
Structure of a cell |
|
Size |
Description |
4 |
Page number of the left child. Omitted if leaf flag bit is set. |
var(1–9) |
Number of bytes of data. Omitted if the zerodata flag bit is set. |
var(1–9) |
Number of bytes of key. Or the key itself if intkey flag bit is set. |
* |
Payload |
4 |
First page of the overflow chain. Omitted if no overflow. |
對於內部頁面,每個單元包含4個字節的左孩子頁面指針;對於葉子頁面,單元不需要孩子指針。接下來是數據的字節數,和關鍵字的長度,下圖描述了單元格式:(a)一個單元的格式 (b)負載的結構。
2.3、溢出頁面
小的元組能夠存儲在一個頁面中,但是一個大的元組可能要擴展到溢出頁面,一個單元的溢出頁面形成一個單獨的鏈表。每一個溢出頁面(除了最后一個頁面)全部填充數據(除了最開始處的4個字節),開始處的4個字節存儲下一個溢出頁面的頁面號。最后一個頁面甚至可以只有一個字節的數據,但是一個溢出頁面絕不會存儲兩個單元的數據。
溢出頁面的格式:
2.4、實例分析
數據庫為test.db,其中有一個表和索引如下:
CREATE TABLE episodes( id integer primary key,name text, cid int); CREATE INDEX name_idx on episodes(name);
2.4.1、葉子頁面格式分析
episodes表的根頁面為第2個頁面(此時episodes表只占一個頁面),表中的數據如下:
sqlite> select * from episodes; 1|Cinnamon Babka|2 2|Mackinaw Peaches|1 3|Mackinaw Peaches|1 4|cat|1 5|cat|1 6|cat|1 7|cat|1 8|cat|1 9|cat|1 10|cat|1 11|cat|1 12|cat|1 13|cat2|40 14|hustcat|5 15|gloriazzz|41 16|eustcat|5 17|xloriazzz|41 |
下面為2號頁面頁面頭(開始的8個字節):
Offset |
Size |
值 及含義 |
0 |
1 |
0x0D: 1: intkey, 2: zerodata, 4: leafdata, 8: leaf(1+4+8) |
1 |
2 |
0x0000:第一個空閑塊的偏移為0 |
3 |
2 |
0x0011:頁面的單元數為17 |
5 |
2 |
0x031C:單元內容區的第一個字節的偏移(距頁面起始位置) |
7 |
1 |
0x00:碎片字節數 |
8 |
4 |
Right child (the Ptr(n) value). Omitted on leaves. |
來看第2個頁面的數據(0x400——0x7ff(255字節)):
頁面頭之后為cell指針數組,第一個cell的相對頁面起始位置偏移為0x03EB,即文件的0x07EB。該單元的數據為:
0x13:數據的字節數,19個字節,即04 00 2B … 61 32。
0x01:關鍵字的字節數,對於整型,則為關鍵字本身,即1。
0x04:從該字節開始為payload,即記錄。0x04為記錄頭的大小,即04 00 2B 00為記錄頭。
0x00:NULL,id字段的值,由於關鍵字保存在key size中,這里為NULL。
0x2B:name字段值的長度,為字符串,長度為(43-13)/2=15。后面15字節43 69 ... 61 32為name字段值。
0x00:NULL,第一條記錄cid的值。
2.4.2、索引頁面格式
sqlite> select * from sqlite_master; table|episodes|episodes|2|CREATE TABLE episodes( id integer primary key,name tex t, cid int) index|name_index|episodes|3|CREATE INDEX name_index on episodes(name)
第3個頁面保存表episodes的索引(也只占一個頁面)。
前8個字節為頁面頭:
0x0A:leaf+zerodata,表示葉子頁面,且頁面中只有關鍵字,沒有數據(即索引頁面)。
0x0000:表示第一個空閑塊的偏移為0。
0x0011:頁面的單元數(記錄數),該頁面含有17個記。
0x030D:單元內容區的第一個字節的偏移(距頁面起始位置)。
0x00: 碎片字節數。
接下來34個字節為17個單元(記錄)的指針數組。第一個單元的偏移為0x03EC,如
來看看索引單元的格式:
0x13:數據的字節數,19個字節,從0x03開始。
0x03:記錄頭的字節數。即03 2B 01為記錄頭。
0x2B:第一個字段的長度,15個字節,該索引是對episodes表的name字段建的,其值為episodes表name字段的值。
0x01:第二個字段的長度,其值為0x01,即episodes表中的對應記錄rowid的值。
2.4.3、索引與order by(索引與查詢優化的關系)
Order by是查詢中經常用到的,一些通用DBMS(比如DM,Mysql)都提供基於索引的形式來實現Order by。SQLite也是通過索引來實現Order by的。當字段有索引時,則直接通過索引很容易實現排序;另一方面,如果排序的字段沒有索引,則以該字段為索引(這種情況下是聚集索引)建立一張臨時表,再將臨時表按順序輸出。來看看sqlite的實現吧。
在SQLite中,默認以rowid來建立聚集索引(對於沒有整型值主鍵的情況)。如果主鍵字段為整型,則將其直接保存在rowid中,實現聚集索引;另一方面,如果主鍵是字符串,則對主鍵建立二級索引。非主鍵的索引都屬於二級索引。
先來看看以整型ID為主鍵的情況:
//以ID(rowid)為索引(即聚集索引) sqlite> explain select * from episodes order by id; 0|Trace|0|0|0|explain select * from episodes order by id;|00| 1|Noop|0|0|0||00| 2|Goto|0|13|0||00| 3|SetNumColumns|0|3|0||00| 4|OpenRead|0|2|0||00| //打開表episodes,p2(=2)為其根頁面 5|Rewind|0|11|0||00| //游標指向第一條記錄 6|Rowid|0|1|0||00| //取出記錄的rowid 7|Column|0|1|2||00| //取出第1列的值 8|Column|0|2|3||00| //取出第2列的值 9|ResultRow|1|3|0||00| //生成記錄結果 10|Next|0|6|0||01| //取下一條記錄 11|Close|0|0|0||00| 12|Halt|0|0|0||00| 13|Transaction|0|0|0||00| 14|VerifyCookie|0|2|0||00| 15|TableLock|0|2|0|episodes|00| 16|Goto|0|3|0||00|
屬性有索引的情況:
//排序的實現——有索引 //算法思想: //(1)從索引中依次讀取記錄(索引記錄的形式如:原索引屬性-rowid的鍵值),並取出rowid. //(2)根據(1)中取出的rowid,在原表中查找記錄,並生成記錄結果. sqlite> explain select * from episodes order by name; 0|Trace|0|0|0|explain select * from episodes order by name;|00| 1|Noop|0|0|0||00| 2|Goto|0|18|0||00| 3|SetNumColumns|0|3|0||00| 4|OpenRead|0|2|0||00| //打開表,p1為表游標(0),p2為表根頁面 5|SetNumColumns|0|2|0||00| 6|OpenRead|2|3|0|keyinfo(1,BINARY)|00| //打開索引,p1為索引游標,p2為根頁面 7|Rewind|2|15|1|0|00| 8|IdxRowid|2|1|0||00| //從索引記錄中取出rowid 9|Seek|0|1|0||00| //根據rowid從表中查找記錄 10|IdxRowid|2|2|0||00| 11|Column|2|0|3||00| 12|Column|0|2|4||00| 13|ResultRow|2|3|0||00| 14|Next|2|8|0||00| 15|Close|0|0|0||00| 16|Close|2|0|0||00| 17|Halt|0|0|0||00| 18|Transaction|0|0|0||00| 19|VerifyCookie|0|2|0||00| 20|TableLock|0|2|0|episodes|00| 21|Goto|0|3|0||00|
對於沒有索引的屬性排序:
//排序的實現——沒有索引 //算法思路: //(1)按查詢屬性為聚集索引建立一個臨時表. //(2)按索引順序輸出結果. sqlite> explain select * from episodes order by cid; 0|Trace|0|0|0|explain select * from episodes order by cid;|00| 1|OpenEphemeral|1|3|0|keyinfo(1,BINARY)|00| //p1為臨時表游標,p2為臨時表列數 2|Goto|0|30|0||00| 3|SetNumColumns|0|3|0||00| 4|OpenRead|0|2|0||00| //打開表episodes 5|Rewind|0|16|0||00| //游標移到表的第1條記錄,p1為游標下標 6|Rowid|0|1|0||00| //p1為表的下標,p2指向表的記錄 7|Column|0|1|2||00| //讀取表p1(=0)的第1列 8|Column|0|2|3||00| //讀取表p1(=0)的第2列 9|MakeRecord|1|3|4||00| 10|SCopy|3|5|0||00| 11|Sequence|1|6|0||00| 12|Move|4|7|1||00| 13|MakeRecord|5|3|8||00| 14|IdxInsert|1|8|0||00| //該指令在索引中插入記錄,相當於對表的Insert. p1為索引下標,即OpenEphemeral打開的臨時表 15|Next|0|6|0||01| 16|Close|0|0|0||00| //關閉表episodes 17|SetNumColumns|0|3|0||00| 18|OpenPseudo|2|1|0||00| //打開臨時表 19|Sort|1|28|0||00| //與Rewind功能類似 20|Column|1|2|4||00| 21|Integer|1|8|0||00| 22|Insert|2|4|8||00| 23|Column|2|0|1||00| 24|Column|2|1|2||00| 25|Column|2|2|3||00| 26|ResultRow|1|3|0||00| //輸出臨時記錄 27|Next|1|20|0||00| 28|Close|2|0|0||00| 29|Halt|0|0|0||00| 30|Transaction|0|0|0||00| 31|VerifyCookie|0|2|0||00| 32|TableLock|0|2|0|episodes|00| 33|Goto|0|3|0||00|