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:zdocdoc_shahsqlitesqlite-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.1Page1File 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-tree1.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的起始地址,然后每段字节数据的解析和对对应关系。
如果觉得我的文章对您有用,请打赏。您的支持是对我莫大的认可! (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |