SQLite剖析之存儲模型


前言

  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|

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM