热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

1.SQLITE存储分析1.1SQLITE存储分析1.1.1存储结构介绍SQLite有3类数据库。除内存数据库外,SQLite把每

 

 

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的起始地址,然后每段字节数据的解析和对对应关系。






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




推荐阅读
  • MyBatis多表查询与动态SQL使用
    本文介绍了MyBatis多表查询与动态SQL的使用方法,包括一对一查询和一对多查询。同时还介绍了动态SQL的使用,包括if标签、trim标签、where标签、set标签和foreach标签的用法。文章还提供了相关的配置信息和示例代码。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • Windows7 64位系统安装PLSQL Developer的步骤和注意事项
    本文介绍了在Windows7 64位系统上安装PLSQL Developer的步骤和注意事项。首先下载并安装PLSQL Developer,注意不要安装在默认目录下。然后下载Windows 32位的oracle instant client,并解压到指定路径。最后,按照自己的喜好对解压后的文件进行命名和压缩。 ... [详细]
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • EPICS Archiver Appliance存储waveform记录的尝试及资源需求分析
    本文介绍了EPICS Archiver Appliance存储waveform记录的尝试过程,并分析了其所需的资源容量。通过解决错误提示和调整内存大小,成功存储了波形数据。然后,讨论了储存环逐束团信号的意义,以及通过记录多圈的束团信号进行参数分析的可能性。波形数据的存储需求巨大,每天需要近250G,一年需要90T。然而,储存环逐束团信号具有重要意义,可以揭示出每个束团的纵向振荡频率和模式。 ... [详细]
  • baresip android编译、运行教程1语音通话
    本文介绍了如何在安卓平台上编译和运行baresip android,包括下载相关的sdk和ndk,修改ndk路径和输出目录,以及创建一个c++的安卓工程并将目录考到cpp下。详细步骤可参考给出的链接和文档。 ... [详细]
  • 本文介绍了P1651题目的描述和要求,以及计算能搭建的塔的最大高度的方法。通过动态规划和状压技术,将问题转化为求解差值的问题,并定义了相应的状态。最终得出了计算最大高度的解法。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
  • 本文介绍了Android 7的学习笔记总结,包括最新的移动架构视频、大厂安卓面试真题和项目实战源码讲义。同时还分享了开源的完整内容,并提醒读者在使用FileProvider适配时要注意不同模块的AndroidManfiest.xml中配置的xml文件名必须不同,否则会出现问题。 ... [详细]
  • 前景:当UI一个查询条件为多项选择,或录入多个条件的时候,比如查询所有名称里面包含以下动态条件,需要模糊查询里面每一项时比如是这样一个数组条件:newstring[]{兴业银行, ... [详细]
  • C++字符字符串处理及字符集编码方案
    本文介绍了C++中字符字符串处理的问题,并详细解释了字符集编码方案,包括UNICODE、Windows apps采用的UTF-16编码、ASCII、SBCS和DBCS编码方案。同时说明了ANSI C标准和Windows中的字符/字符串数据类型实现。文章还提到了在编译时需要定义UNICODE宏以支持unicode编码,否则将使用windows code page编译。最后,给出了相关的头文件和数据类型定义。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • Python爬虫中使用正则表达式的方法和注意事项
    本文介绍了在Python爬虫中使用正则表达式的方法和注意事项。首先解释了爬虫的四个主要步骤,并强调了正则表达式在数据处理中的重要性。然后详细介绍了正则表达式的概念和用法,包括检索、替换和过滤文本的功能。同时提到了re模块是Python内置的用于处理正则表达式的模块,并给出了使用正则表达式时需要注意的特殊字符转义和原始字符串的用法。通过本文的学习,读者可以掌握在Python爬虫中使用正则表达式的技巧和方法。 ... [详细]
  • 【shell】网络处理:判断IP是否在网段、两个ip是否同网段、IP地址范围、网段包含关系
    本文介绍了使用shell脚本判断IP是否在同一网段、判断IP地址是否在某个范围内、计算IP地址范围、判断网段之间的包含关系的方法和原理。通过对IP和掩码进行与计算,可以判断两个IP是否在同一网段。同时,还提供了一段用于验证IP地址的正则表达式和判断特殊IP地址的方法。 ... [详细]
author-avatar
mobiledu2502894873
这个家伙很懒,什么也没留下!
Tags | 热门标签
RankList | 热门文章
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有