B-Tree的介绍与数据库中应用分析



导读:在一个有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的几点建议:

  1. 保持顺序遍历
  2. 使用层级索引,达到最小的磁盘读操作
  3. 使用部分结点是满的,部分结点不满来提升插入和删除时的性能
  4. 使用简洁的递归算法维持索引的平衡性。

本文主要介绍了B-Tree的一些基本概念,对B-Tree先有一个大概的认识,后面增加如何写代码创建一个B-Tree,并且理解B-Tree中插入分裂,删除合并的问题。

以上内容70%翻译自维基百科,翻译错误或者个人理解有误支出还请指出,不要让我误导了他人才好,谢谢!



时间: 2024-10-16 10:15:06

B-Tree的介绍与数据库中应用分析的相关文章

Eclipse中java向数据库中添加数据

前面详细写过如何连接数据库的具体操作,下面介绍向数据库中添加数据. 注意事项:如果参考下面代码,需要 改包名,数据库名,数据库账号,密码,和数据表(数据表里面的信息) 1 package com.ningmeng; 2 3 import java.sql.*; 4 5 /** 6 * 1:向数据库中添加数据 7 * @author biexiansheng 8 * 9 */ 10 public class Test01 { 11 12 public static void main(String

SQL中的where条件,在数据库中提取与应用浅析

来源:深入MySQL内核 1        问题描述 一条SQL,在数据库中是如何执行的呢?相信很多人都会对这个问题比较感兴趣.当然,要完整描述一条SQL在数据库中的生命周期,这是一个非常巨大的问题,涵盖了SQL的词法解析.语法解析.权限检查.查询优化.SQL执行等一系列的步骤,简短的篇幅是绝对无能为力的.因此,本文挑选了其中的部分内容,也是我一直都想写的一个内容,做重点介绍: 给定一条SQL,如何提取其中的where条件?where条件中的每个子条件,在SQL执行的过程中有分别起着什么样的作用

如何将数据库中存的树转化为树形列表(以easyui的tree为例)

很多时候,我们会把一棵树存放到数据库中,当前台需要展示一个树形列表时,将这棵树读取出来并显示,这个过程是怎么实现的呢? 这篇文章是以构造一棵easyui前台框架的一个树形列表为例,后台框架是spring MVC+JPA. 首先看一下数据库中这颗树是怎么存的: 树的结构一目了然,这是一棵表示部门的树. 下面是实体类: /** * 部门类 用户所属部门(这里的部门是一个相对抽象的词) * 使用前缀编码,每级增加三个数字,如:第一级 001,第二级001001,第三级001001001 * @auth

SQL数据库中的主键与外键的介绍

一.什么是主键.外键: 关系型数据库中的一条记录中有若干个属性,若其中某一个属性组(注意是组)能唯一标识一条记录,该属性组就可以成为一个主键比如 : 学生表(学号,姓名,性别,班级) 其中每个学生的学号是唯一的,学号就是一个主键 用户表(用户名.密码.登录级别) 其中用户名是唯一的, 用户名就是一个主键 上机记录表(卡号,学号,姓名.序列号) 上机记录表中单一一个属性无法唯一标识一条记录,学号和姓名的组合才可以唯一标识一条记录,所以 学号和姓名的属性组是一个主键 上机记录表中的序列号不是成绩表的

Mysql数据库中 User表权限字段说明全介绍

一:mysql权限表user字段详解: Select_priv.确定用户是否可以通过SELECT命令选择数据. Insert_priv.确定用户是否可以通过INSERT命令插入数据. Update_priv.确定用户是否可以通过UPDATE命令修改现有数据. Delete_priv.确定用户是否可以通过DELETE命令删除现有数据. Create_priv.确定用户是否可以创建新的数据库和表. Drop_priv.确定用户是否可以删除现有数据库和表. Reload_priv.确定用户是否可以执行

java根据数据库中的数据 的list 生成 tree 型 json

这些代码 本来 也是 我网上找的但是 很遗憾 ,用数据源取到的list ,无法正常返回到前台,list 转json 的时候 就会 出错 ,我展示 正确的代码 1. package com.labci.javamail.test; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; /** * 节点类 */ class Node { /

MySQL数据库中的索引(一)——索引实现原理

今天我们来探讨一下数据库中一个很重要的概念:索引. MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构,即索引是一种数据结构. 我们知道,数据库查询是数据库的最主要功能之一.我们都希望查询数据的速度能尽可能的快,因此数据库系统的设计者会从查询算法的角度进行优化.最基本的查询算法当然是顺序查找(linear search),这种复杂度为O(n)的算法在数据量很大时显然是糟糕的,好在计算机科学的发展提供了很多更优秀的查找算法,例如二分查找(binary searc

在数据库中存储层级结构

(摘自:http://qinxuye.me/article/storing-hierachical-data-in-database/) 本文参考自这篇文章.文章是2003年的,但是现在来看仍然有着实际意义. 层级结构,也叫树形结构.在实际应用中,你经常需要保存层级结构到数据库中.比如说:你的网站上的目录.不过,除非使用类XML的数据库,通用的关系数据库很难做到这点. 对于树形数据的存储有很多种方案.主要的方法有两种:邻接表模型,以及修改过的前序遍历算法.本文将会讨论这两种方法的实现.这里的例子

JDBC的介绍和数据库的连接

声明:本栏目所使用的素材都是凯哥学堂VIP学员所写,学员有权匿名,对文章有最终解释权:凯哥学堂旨在促进VIP学员互相学习的基础上公开笔记. JDBC的介绍: 1.JDBC设计理念: java依赖数据库去存储程序需要的数据,我们用java写好一个程序后,我们会把这个程序所需要的数据都存储到数据库中,当客户运行程序时,程序需要读取数据库中的数据,实现这个操作的就是JDBC.而为了规范,为了统一管理,把责任都转给数据库开发商(在接口中详细讲解过),java只负责写出一系列的接口,至于怎么去实现[学Ja