导读:在一个有100万条记录的数据表中,利用二分查找定位一条记录,大概需要20次操作,理论上也就是20次磁盘读操作,需要花费大概0.2秒,有没有办法将磁盘操作次数降到3次呢?下面我们就介绍一下如何将20次的操作降到3次。
关于B-Tree的一些基本介绍
在计算机科学中,B-Tree是一种自平衡的树型数据结构,它保持数据有序,并且允许在O(log n)时间内进行检索,顺序访问,插入以及删除操作。B-Tree是一种允许节点的子节点个数大于2的泛化二叉搜索树(BST)。不同于自平衡二叉查找树,对于大数据块读写操作的系统来说,b-Tree是一种最优的选择。B-Tree是一个很好的外部存储的数据结构的例子,因此,它通常被应用于数据库和文件系统。
在B-Tree中,内部节点(非叶子节点)有可变个子节点,这个数目是在一个预定义的范围内的。当数据插入一个内部节点或者从一个内部节点的子节点中删除时,这个内部节点的子节点的个数就会发生改变,这个时候这个内部节点有可能面临合并(删除时)或者分裂(插入时)。因为B-Tree的内部节点的子节点个数是在一个范围内的,因此,不需要像其他平衡搜索树(BST,AVL,RB-Tree)一样频繁的进行再平衡操作。正因为如此,当一个内部节点的子节点并不饱满(达到最大子节点树)时,就会导致了一部分空间的浪费(虽然,内部节点的子节点个数是个可变的值,但是一个内部节点所能包含的最大子节点的个数一般是固定的,四叉树或者八叉树等,因为,对于一棵四叉树来说,如果其子节点只有两个,就浪费了两个节点的空间)。2-3树为例
B-Tree的每一个内部节点都包含了一些keys,keys代表的是分裂子树的时候的分裂值(其实就是根绝keys的个数来判断有几个子节点)。例如,如果一个内部节点有三个子节点,那么它就应该有两个keys:a1和a2。左边子树的所有值都是小于a1这个值,中间子树的所有数据都是大于a1并且小于a2的,而右边子树的所有数据都是大于a2的。
如上图,两个keys分别是7和19,有三个子节点(也可以说是三棵subtree)。
通常,keys的分数应该在d到2d之间,d代表的是最小的keys个数,d+1就代表了这棵树的最小度(度的下限)或者说是最小的分支数。事实上,在一个结点中,keys占据了大多数的空间。The
factor of 2 will guarantee that nodes can be split or combined.(着实不知道怎么翻译好,个人理解大概的意思是说之所以范围是d到2d,因为这样能保证分裂或者合并的时候满足B-Tree的性质,后面会说到B-Tree的性质)。如果一个内部节点有2d个keys,那么再往这个内部节点增加数据的时候,这个节点就要分裂成两个节点,而且这两个节点的keys的个数都是d,这个节点分裂前中间的结点要插入到其父节点中(具体的插入过程,后面会详细介绍)。这样,分裂出来的两个节点都满足最少有d个keys这一性质。同理,如果一个结点(B)只有d个keys,邻居(兄弟)节点(C)也只有d个keys,而且现在又要从这个节点(B)中删除一个keys,那么这个结点就要和它的邻居节点合并成一个新节点(D),合并完事以后,本来D应该有2d-1个结点,但是因为两个节点合并,影响了父节点(A),所以,父节点中就得降下来一个结点,插入到合并的结点D中,D中实际上应该是满的,即2d个元素。(为什么会影响到父节点?要降下来一个结点呢?因为原来的节点B与C是父节点的两个分支,比如,B里面放的都是小于10的数据,C节点里面放的都是大于10的数据,那么,在父节点A中,定然存在一个keys,且值为10,一旦B与C合并了,父节点中的这个值是10的keys就是去意义了,因此要将来,放到新合并成的结点D中。)
在一个节点里,其分支个数(子节点分数)的值始终并这个节点包含的keys的个数多一个。在一棵2-3树中,一个内部节点可能会存储1个keys(对应2个分支)或者2个keys(对应3个分支)。一个B-Tree有时可以被描述成(d+1)-(2d+1)树或者用最大分支数2d+1,比如2-3
tree,2代表的是d+1,3代表的是2d+1,这里d就是1,也可以把2-3
tree叫做 3-tree。
B-Tree是一棵平衡树,因此需要它所有叶子节点的深度一样。在往树中增加元素的时树的深度慢慢增加,但是并不频繁,并且即使深度增长,结果只会让叶子节点到根节点的距离增长1而已。
在访问数据远比操作数据多的情况下,B-Tree无疑在现实的选择中占有很大的优势。because
then the cost of accessing the node may be amortized overmultiple operations within the node.因为访问结点的消耗会被平摊到节点内部的多重操作上。这种情况经常发生,当节点数据是存放在二级存储器上时,如在磁盘上时。最大化keys的个数可以使的树的高度变小,并且节点的访问也会减少。另外,树的再平衡操作也是不常发生的。这个最大化keys个数具体是多少,这要根据每个子节点所存储信息的多少以及磁盘块的大小或者类似的二级存储器的数据块存储大小来决定。尽管2-3B-Tree很容易解释,事实上在二级存储器上使用B-Tree时需要把子节点的个数增大一些,以便达到提升性能的目的(实际上就是说,子节点越多,树的高度就越低,这样遍历的深度就越少,但是具体要设置多少个节点,就要根据节点中存储的数据信息大小和外部存储器,磁盘来决定,尽量将一个结点的大小达到一个磁盘块的大小,这里就涉及到了磁盘读写的原理问题了,详细参见http://blog.csdn.net/hbhhww/article/details/8206846)。
B-Tree在数据库中的使用
通常情况下,对于排序和搜索算法,用比较运算操作的次数的大O形式来表示。(比如,O(n)表示进行了n次的比较运算),例如在一个有N条记录的排序表中使用二分查找,会做次运算。如果这个表是包含了100W条记录,那么想要定位到一个指定的记录,就需要20次操作:.
数据库数据是放在磁盘驱动器上的。从磁盘驱动上读取一个记录的时间远远超过了验证这个数据是否有效的时间。从磁盘上读取一个记录所需要的时间包含一次寻道时间和一次旋转延迟。寻道的时间可能是在0到20,或者更多毫秒,旋转延迟的时间平均下来差不多是一个旋转周期的一半时间(最多旋转1圈,最少不用旋转,平均情况下,需要旋转半圈)。一个7200(转
/每分钟)的硬盘,每旋转一周所需时间为60×1000÷7200=8.33毫秒,则平均旋转延迟时间为8.33÷2=4.17毫秒。例如希捷ST3500320NS型号的硬盘,他的track-to-track
seek time是0.8毫秒,average reading seek time是8.5毫秒。这里,我们先简单的认为,一次读取操作所需要的时间大概是10毫秒左右的样子。
对于一个有100W记录的数据库表,我们定位一个记录需要进行20次操作(二分查找),一次操作平均需要10毫秒,那么成功定位一条记录就需要花费0.2秒。
事实上,是花费不了那么长时间的。因为我们知道,磁盘读写的最小单位是disk block,一个磁盘块的大小假如是16KB,如果一个记录的大小是160B,那么一个磁盘块大约可以容纳100个记录,这样在有100W记录的表中利用二分查找时,当二分14次以后,其实剩下的查找范围只有大约61个记录了,这61个记录如果都放在同一个磁盘块中,那么最后6次所用的时间相当于一次磁盘读取所用的时间。
最后6次只需要一次磁盘访问,而前面的14次各需要一次磁盘访问,这样还是比较慢,如果还想获得更快的速度,那么只能对前14次的操作进行优化。这就涉及到了数据库的索引了。
下面我也来见证一下奇迹,看看如何将20次磁盘读写缩减到3次的。前面的例子中,我们假设表中有100W条记录,,理论上定位一个记录需要20次磁盘读写,实际上可能只需要15次,有没有办法再提高一下性能呢?有,那就是建立一个稀疏索引表。原始表中的数据存储时,是100个记录存放在一个磁盘块中的,那么我们将每个磁盘块的第一条记录拿出来,做成一个表(保证这个表的记录还是有序的),这个表就是稀疏表,这个稀疏表中的数据量是原始表的1%,也就是1W条记录,我们在这一万条记录中利用二分查找,只需要8次磁盘操作就可以定位到指定的block,定位一个具体的记录,那就只需9次磁盘操作即可完成。
同理,我们还可以对这个稀疏表再进行稀疏,10000条记录的表正好构成了100条记录的二次稀疏索引表,我们先在二次稀疏表中定位一次,定位到了稀疏表中,这时花费了一个磁盘读写时间,在稀疏表中再定位一次,又花了一个磁盘读写时间,再去在原始表中定位具体的一条记录,还是只需要一次磁盘操作,那么,利用二次稀疏索引表,总共只需要3次磁盘操作,就完成了一条原始表中的数据的定位。(以上操作的前提是,原始表是个顺序表,稀疏表和二次稀疏表也是有序的)。下面贴个简略图
从上图是不是看到了B-Tree的影子。
上面介绍的是定位一个记录的具体时间花费,通过以前我们对查找树的学习,可以发现,查找一般是最简单的操作,当然,这里的简单是代码逻辑简单,其实B-Tree的插入和删除操作逻辑上也比查找都要麻烦。不过现在先说说插入和删除的时间花费问题。
如果在一个顺序表中插入一条记录,就需要把插入位置之后的所有记录都要后移,如果删除的话,就需要把删除的记录后面的所有记录前移,这是非常浪费时间的操作。而且我们前面建立的索引表也需要更新。有没有什么办法来减少这种耗时的操作呢?这就让我们又想到了计算机科学“时间和空间”问题。是的,好的办法就是牺牲空间,来提升性能。像上面说的,如果每一个block都存放慢慢的100条记录,空间利用率是100%,可是再添加一条数据的时候,是不是就麻烦了,要保证数据有序,但是block中的数据又都是满的,只能重新分配磁盘块了。所以,我们应当给每个block“预留”一定的空间,这样插入数据是,就不会面临上面那个“牵一发而动全身”的问题了,同理,删除的时候,就直接删除那条记录,对于block并不进行操作,虽然block上会“空出”一些空间,但是到达了提升性能的任务。这就需要在时间和空间上能做到一个比较好的平衡。B-Tree一个结点的keys个数是在d到2d之间,小于d时合并,大于2d时分裂,也就是说,一个block的空间利用率应该是在50%到100%之间的。
数据库中使用B-Tree的几点建议:
- 保持顺序遍历
- 使用层级索引,达到最小的磁盘读操作
- 使用部分结点是满的,部分结点不满来提升插入和删除时的性能
- 使用简洁的递归算法维持索引的平衡性。
本文主要介绍了B-Tree的一些基本概念,对B-Tree先有一个大概的认识,后面增加如何写代码创建一个B-Tree,并且理解B-Tree中插入分裂,删除合并的问题。
以上内容70%翻译自维基百科,翻译错误或者个人理解有误支出还请指出,不要让我误导了他人才好,谢谢!