sqlite3存储格式
本篇介绍sqlite3数据库文件的存储格式。
通过阅读源读源代码可以知道sqlite的设计思想。
一个sqlite数据库文件对应着一个数据库。sqlite将数据库文件划分大小一致的存储(以区分内存)页面,并通过一系列数据结构将它们组织起来。
sqlite组织页面的数据结构主要有B树和二维链表。每一个页面要么是B树的叶子或结点,要么是二维链表的一个节点。用作B树的页面都有8或12字节的页面信息头,特别地第一个页面除了是表的根结点是数据库文件的根,所以它首先包含100字节的是库信息,然后才是作为一个是B树的根结点。另一方面闲置的页面没有页面头,它的链头是在第一个页面的库信息头。二维链表的第一维由单向链表实现,第二维由数组实现。用作第一维节点的页面也是一个空闲页面,被统计在库信息头。
数据库层面的表和索引,在底层存储格式中用B树结构组织,第一张表或索引都对应着一棵B树。底层存储中所有空闲(闲置)页面,都由二维链表链接起来。
B树页面,有统一的存储格式。首先是页面信息头,然后紧跟着页内数据排序索引(请区分数据库的表索引,这里是数据结构中的外部排序索引)数组;而页内数据存储在页面的尾部。数据索引和数据单元分别从正反两个方向向页面中间伸展,页面中间部分形式空闲区域。
sqlite数据库文件使用自然字节序存储数据,这种字节序有利于它里面使用到可变长整形解释。
各种具体的数据结构和详细的格式编排请自行参阅源代码,不一一贴上划水。自行用任意一门编程语言写程序遍历解读sqlite数据库文件其中的内容。
在这里定义本篇使用的一些术语:
页面,数据库文件页面,请区分内存页面或虚地址空间页面,尽管sqlite默认页面大小也是4KB。
指针,请区分c语言指针,这里指文件内地址索引,或页面内地址偏移。
单元,cell,存储在B树页面的单位数据。
整型主键,sqlite表默认的自增计数。
变长整型,sqlite存储中使用到的一种基于自然字节序的变长整数存储方式。每字节低7位为有效数值位,高位标记下一字节是否继续为当前变长整数的一部分,高位为0表示为变长整数的最后一个字节。
叶子,B树的叶子结点。
结点,B树的非终端结点。
索引,当提及结构化查询时指索引表,而当提及低层存储格式时等同于编号,地址和指针。
前面总括了底层存储组织,现在结合上一层数据库层来看存储组织,库和表的存储。sqlite将一切表和索引视作表,并以B树的形式保存在数据库文件。sqlite数据库默认有一张管理表,名为sqlite_master。这张表记录着用户创建的所有表和索引的名字,左联合表名,表的根页面索引号以及创建sql语句。也就是数据库文件的第一页面一棵入口B树的根,通过这棵树可以索引到其它B树(数据库层面表或索引)的根。当sqlite打开某一张表时,必须遍历管理表找出与表名匹配的记录,从而找出记录中根页面索引号,才能定位到表的入口位置。索引和表是分开存储的,表默认使用是自增计数,因为存储在B树中,所以每条记录都必须有一个唯一键。索引就是一张索引键与自增计数的映射表。当使用索引查询记录时,就相当于索引左联合到数据记录的表,条件为自增计数。总结来说,就是sqlite数据库文件里保存了多于一棵的B树,第一棵B树入口位于第一页面,其它B树的根必须通过第一棵B树才能索引出来。
首先我们来看sqlite数据库的总起信息,也就是入口,位于第一页面的开头100字节。
<img dbheader/>
可看到数据库文件开头作了文件类型标记“SQLite format 3\0”。当前数据库以4KB尺寸来划分页面。数据库文件迄今被修改过461次,并且没有空闲页面。当前默认编码方式为UTF-8。
接着就是第一个页面作为B树根的页面信息。
页面信息首先定义了当前页面的类型,包含自增计数的非叶子结点并且有左孩子记录。跟着就是当前页面的状态信息,有1条记录(单元),下一条插入的记录(单元)的位置,结点的右孩子位于索引号为141的页面(也就是第141个页面)。当“number of cells on this page”和“first byte of the cell content area”越靠近,表示页面内空闲空间越小。
下面是对第一个页面的入口B树跟踪。
第一行是当前页面号为1, 类型是结点,右孩子在141号页面,左孩子分别是139号页面以23为key,只有一个左孩子。
第二行是当前页面号为139,类型是叶子,存放23条记录。根据上下文知,是1号页面的左孩子,并且存放小于等于key23的记录,一共有23条记录。
第三行是当前页面号为141,类型是叶子,存放23条记录。根据上下文知,是1号页面的右孩子,是刚刚分裂成139和141两个叶子,各存放一半数量的记录。
下面是第139号页面,作为左孩子叶子页面的跟踪。
绿色框框是我们关心的信息,描述了记录在当前页面的指针,记录长度以及唯一键值(自增计数)。根据上下文139号页面,这个B树叶子存放自增计数小于等于23的记录一共23条。
蓝色框框是记录单元里的数据,是表的根页面指针,在当前情景中,2,4,5,11和12都是某张表的根页面索引号。
下面我们对2号页面为根的表进行跟踪。
分别对2,4以及12号页面为根三张表的B树存储组织,蓝色框框叶子记录总数与使用sqlite查询的结果一致。
最后我们跟踪一下空闲页面,在本篇中使用的数据库不包含空闲页面,现在我们truncate一个表以产生一些空闲页面,就拿上面举例的12号页面为根的表,这张表包含了1个根结点和6个叶子,其中5个叶子是左孩子,1个叶子是右孩子。
留意绿色框框,文件修改次数被加1,刚刚有一个表被清空了。
蓝色框框显示了本次清空动作,产生了8个空闲页面,而链接这些页面的第一节点在第60号页面。
红色框框显示了放在60号页面节点的页面索引号,对照上面12号页面为根的表跟踪,可以看到被清空的表的存储B树所使用到的页面都被链接到空闲链表。
介绍完以页面为单位存储大框架后,我们再来看页面内的存储。
页面分为三部分,页面信息头,单元(cell)指针(索引)数组,以及数据(记录)单元。索引和数据分别放于页面的一头一尾,中间为未使用空间,各自向中间获取分配空间来应对增长。当某一单元被删除,它的空间会被回收链入到页面内空闲块链表。当空闲块再次被使用时可能规格不一致,从而会产生碎片(零碎字节),这些碎片将不能被重用,因而必须记录下来,供往后页面零碎程序评级用。我猜过于零碎的页面可能会在合适的时机进行重整。也就是单元区域遍历所有单元,必须依赖页面内索引数组。只有索引数组内索引到的单元才是有效的。对于未曾发生过单元回收重用的页面,单元区域可以直接遍历,但发生过单元回收后则不可,必须依赖页面内索引。
虽然说管理表是以B树存储,但是却不是以表名为键而只是单纯的自增计数,所以当表(包括索引,视图等)的数量多的时候,打开其中之一也没有得益于B树结构。
sqlite数据库文件使用的B树是一种B-树。参考我们曾经的教材严女士的《数据结构》对B-树的定义。
一棵m阶的B-树,或为空树,或为满足下列特性的m叉树:
(1)树中每个结点至多有m棵子树;
注:这是由sqlite数据库页面大小限制,对于Key为变长时,并不是固定m阶,而是一个泛数多阶。
(2)若根结点不是叶子,则至少有两棵子树;
注:当表的首个页面空间可以容纳所有数据时为叶子,当不满足时将分裂出两个叶子,开始变成根结点。
(3)除根之外的所有非终端结点至少有m/2的上限棵子树;
注:当结点容量不足时结点会分裂出两个结点,各迁移一半key。
(4)所有的非终端结点中包售下列信息数据(n,(A1,K1),(A2,K2),...(An,Kn), An+1),其中n为K(ey)的个数,A(ddr)是子树地址。
注:n个Key意味有n个左孩子,A1至An是左孩子页面号,An+1是右孩子页面号。n和An+1保存在页面头,(Ax,Kx)|x<n则是结点页面的数据单元。
(5)所有的叶子都出现在同一层次上,并且不带信息。
注:不带信息也就是页面类型是叶子时,数据单元忽略左孩子域。但sqlite有没有维护树高度未能证明。
通过本篇,相信大家已经对sqlite数据库有了很具体的了解了。有了上面的知识大家就能对sqlite的存储性能进行一定的分析,比如数据库页面的大小对增删改查的影响。