sqlite浅析2-SQLITE存储分析-SQLITE文件格式分析

前端之家收集整理的这篇文章主要介绍了sqlite浅析2-SQLITE存储分析-SQLITE文件格式分析前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

1. sqlITE存储分析

1.1sqlITE存储分析

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',980303008,'13940709836','shandong jinan');

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

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

可以看到生成了一个8K的.db文件,根据后面的分析我们知道,这个db文件有两个page,每个page为4K。

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

可以用sqlitespy打开.db文件,看到我们建立的数据库和表。



还可以用其他编辑工具打开.db文件,以二进制的方式查看文件

1.1表数据页

表数据用B+tree 来存储。

1.1.1Page1

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.1Page2

这里首先就是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.1Index 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的起始地址,然后每段字节数据的解析和对对应关系。






如果觉得我的文章对您有用,请打赏。您的支持是对我莫大的认可

猜你在找的Sqlite相关文章