索引技术
在B树之前,先了解一下索引技术:
许多计算机应用程序都是以大型数据库为中心,而这些数据库因太大只能存在外存中,所以就要求应用程序有高效检索的能力,同时还包括插入,删除,更新等操作能力。
索引就是把一个关键字与它对应的记录相关联的过程,一个索引由若干个索引项构成,每个索引项至少应包含关键字和其对应的记录在存储器中的位置等信息。
索引按照结构可以分为线性索引、树形索引和多级索引。
线性索引
所谓线性索引就是将索引项集合组织为线性结构,也称为索引表。我们重点介绍三个线性索引:稠密索引、分块索引、和倒排索引。
稠密索引
对于稠密索引这个索引表来说,索引一定是按照关键码有序排列的。
索引项有序也就意味着,我们要查找关键字时,可以用到折半、插值、斐波那契等有序查找算法,大大提高效率。
这显然是稠密索引优点,但是如果数据集非常大,比如上亿,那也就意味着索引也得同样的数据集长度规模,对于内存有限的计算机来说,可能就需要反复去访问磁盘,查找性能反而大大下降了。
分块索引
稠密索引因为索引项与数据集的记录个数相同,所以空间代价很大。为了减少索引的个数,我们可以对数据集进行分块,使其分块有序,然后再对每一块建立一个索引项,从而减少索引项的个数。
建立二级索引,检索时,把二级线性索引读入内存。再根据记录中的指针找到相应的一级线性索引(一级线性索引存在磁盘块),把这块读入内存。然后根据二分法对这块进行检索,找到记录在磁盘的位置;最后把所需记录读入,完成检索操作。
倒排索引
为了满足检索某些属性值,比如说一个学生既会打篮球,又会踢足球,长得帅,学习还棒,这都是他的属性值。因为好多人都有其中的某个属性值,所以建立倒排表,把具有某一个属性值的放到一起,这样找某些同时具有属性的人,就求满足这些属性的表的集合,就可以了。这个交集得到是一个|主|关键码,要想得到这个具体对象的其他信息,需要根据这个主关键码来去主表里面去找。
倒排表只能是辅助表,为了方便找到某些属性值来建立的,最后还是要通过获得的主关键码去找到在主文件中的地址指针去存读取记录。
树形索引
树形索引是一种动态索引
静态索引结构是指这种索引结构在初始创建、数据装入时就已经定型,而且在整个系统运行期间,树的结构不发生变化,只是数据在更新。
动态索引结构是指在整个系统运行期间,树的结构随着系统的增删随时调整,以保持最佳的搜索效率。
静态索引结构的优点是结构定型、建立方法简单,存取方便;缺点是不利于更新,插入或者删除时效率低。
动态索引结构的优点是在插入或者删除时能够自动调整索引树结构,以保持最佳的搜索效率;缺点是实现算法复杂。
B树
外存索引树节点在不超过磁盘页块大小限制的前提下,应该尽量增加每个节点内关键码的个数。
大规模数据存储中,实现索引查询这样一个实际背景下,树节点存储的元素数量是有限的(如果元素数量非常多的话,查找就退化成节点内部的线性查找了),这样导致二叉查找树结构由于树的深度过大而造成磁盘I/O读写过于频繁,进而导致查询效率低下(为什么?),那么如何减少树的深度(当然是不能减少查询的数据量),一个基本的想法就是:采用多叉树结构(由于树节点元素数量是有限的,自然该节点的子树数量也就是有限的)。
也就是说,因为磁盘的操作费时费资源,如果过于频繁的多次查找势必效率低下。那么如何提高效率,即如何避免磁盘过于频繁的多次查找呢?根据磁盘查找存取的次数往往由树的高度所决定,所以,只要我们通过某种较好的树结构减少树的结构尽量减少树的高度,那么是不是便能有效减少磁盘查找存取的次数呢?那这种有效的树结构是一种怎样的树呢?
这样我们就提出了一个新的查找树结构——多路查找树。根据平衡二叉树的启发,自然就想到平衡多路查找树结构,B树的各种操作能使B树保持较低的高度,从而达到有效避免磁盘过于频繁的查找存取操作,从而有效提高查找效率。
满足条件:
- 树中每个结点最多含有m个孩子(m>=2);
- 除根结点和叶子结点外,其它每个结点至少有[ceil(m / 2)]个孩子(其中ceil(x)是一个取上限的函数);
- 若根结点不是叶子结点,则至少有2个孩子(特殊情况:没有孩子的根结点,即根结点为叶子结点,整棵树只有一个根节点);
- 所有叶子结点都出现在同一层,叶子结点不包含任何关键字信息(可以看做是外部接点或查询失败的接点,实际上这些结点不存在,指向这些结点的指针都为null);
- 每个非终端结点中包含有n个关键字信息:
a) Ki (i=1…n)为关键字,且关键字按顺序升序排序K(i-1)< Ki。
b) Pi为指向子树根的接点。
c) 关键字的个数n必须满足: [ceil(m / 2)-1]<= n <= m-1。就是每个子节点(除特殊的外)都至少有 [m / 2]-1个,最多m个。
如下图所示:
性质:
如果阶数为3,就是2-3树。
1. 总是树高平衡的,所有叶节点在同一层;
2. 更新和检索只会影响一部分磁盘块,因此性能好;
3. 把关键码相近的值放在一个磁盘块中,利用了访问局部性原理;
4. 保证树中至少有一定比例的节点是满的,即能保证利用率,又使得插入删除不必频繁修改节点,减少了磁盘读取数。
局部性原理是指CPU访问存储器时,无论是存取指令还是存取数据,所访问的存储单元都趋于聚集在一个较小的连续区域中。
时间局部性:程序有在一段时间内多次访问同一个数据块的倾向,这个写程序的都知道;
B+树
B+树是对B树的一种变形树,它与B树的差异在于:
- 所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接。 (而B 树的叶子节点并没有包括全部需要查找的信息)
- 所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。 (而B 树的非终节点也包含需要查找的有效信息),非终端结点只是其子树节点中最大(或最小)的复写。
- 所有的叶子结点和相连的节点使用链表相连,便于区间查找和遍历。
B+ 树的优点在于:
由于B+树在内部节点上不包含数据信息,因此在内存页中能够存放更多的key。 数据存放的更加紧密,具有更好的空间局部性。因此访问叶子节点上关联的数据也具有更好的缓存命中率。
B+树的磁盘读写代价更低
B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。B+树的叶子结点都是相链的,因此对整棵树的便利只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
B+树的查询效率更加稳定
由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
但是B树也有优点
其优点在于,由于B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。
数据库索引采用B+树的主要原因
是 B树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题。正是为了解决这个问题,B+树应运而生。B+树只要遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作(或者说效率太低),B树必须用中序遍历的方法按序扫库,而B+树直接从叶子结点挨个扫一遍就完了,B+树支持range-query非常方便,而B树不支持。
在B-树中,越靠近根节点的记录查找时间越快,只要找到关键字即可确定记录的存在;而B+树中每个记录的查找时间基本是一样的,都需要从根节点走到叶子节点,而且在叶子节点中还要再比较关键字。
从这个角度看B-树的性能好像要比B+树好,而在实际应用中却是B+树的性能要好些。
因为B+树的非叶子节点不存放实际的数据,这样每个节点可容纳的元素个数比B-树多,树高比B-树小,这样带来的好处是减少磁盘访问次数。尽管B+树找到一个记录所需的比较次数要比B-树多,但是一次磁盘访问的时间相当于成百上千次内存比较的时间,因此实际中B+树的性能可能还会好些,而且B+树的叶子节点使用指针连接在一起,方便顺序遍历(例如查看一个目录下的所有文件,一个表中的所有记录等),这也是很多数据库和文件系统使用B+树的缘故。
B*树
B*Tree是B+树的变体,在B+Tree的非根和非叶子结点(内结点)再增加指向兄弟的指针
- B* 树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3(代替B+树的1/2);
- B+树的分裂:当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针;
- B*树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针;
所以,B*树分配新结点的概率比B+树要低,空间使用率更高。