索引是数据库常见的数据结构,每个后台开发人员都应该对索引背后的数据结构有所了解。

本文通过分析B-Tree及B-/+Tree数据结构及索引性能分析及磁盘存取原理尝试着回答一下问题:

  1. 为什么B-Tree适合数据库索引及红黑树的二叉平衡树不适合作为索引
  2. B+Tree比BTree做索引的优势
  3. 为什么MongoDB采用B-Tree作为索引结构而MySQL采用B+Tree作为索引存储结构

B-Tree

B 树(B-Tree)是为磁盘等辅助存取设备设计的一种平衡查找树,它实现了以 \(O(\lg n)\) 时间复杂度执行查找、顺序读取、插入和删除操作。由于 B 树和 B 树的变种在降低磁盘 I/O 操作次数方面表现优异,所以经常用于设计文件系统和数据库。

使用阶来定义 B 树,一棵 m 阶的 B 树,需要满足下列条件:

  1. 每个节点最多拥有m个子节点且m>=2,空树除外
  2. 除根节点外每个节点的关键字数量大于等于ceil(m/2)-1,小于等于m-1,非根节点关键字数必须>=2
  3. 所有叶子节点均在同一层、叶子节点除了包含了关键字和关键字记录的指针外也有指向其子节点的指针只不过其指针地址都为null对应下图最后一层节点的空格子
  4. 如果一个非叶节点有n个子节点,则该节点的关键字数等于n-1
  5. 所有节点关键字是按递增次序排列,并遵循左小右大原则

注:

  1. m阶代表一个树节点最多有多少个查找路径,m阶=m路,当m=2则是2叉树,m=3则是3叉。
  2. ceil()是个朝正无穷方向取整的函数,如ceil(1.1)结果为2,即向上取整。

B 树中的节点分为内部节点(Internal Node)和叶节点(Leaf Node),内部节点也就是非叶节点(Non-Leaf Node)。

B-Tree的查找

B-Tree的查找过程:根据给定值查找结点和在结点的关键字中进行查找交叉进行。

首先从根结点开始重复如下过程:若比结点的第一个关键字小,则查找在该结点第一个指针指向的结点进行;若等于结点中某个关键字,则查找成功;若在两个关键字之间,则查找在它们之间的指针指向的结点进行;若比该结点所有关键字大,则查找在该结点最后一个指针指向的结点进行;若查找已经到达某个叶结点,则说明给定值对应的数据记录不存在,查找失败。

例如:
在一棵 5 阶B-树中查找元素 29

首先29比根节点值大,所以找根节点的右子数,然后再根据值得判断,发现 29 介于 28 和 48 之间,然后在从中间子树继续查找下去。

B-Tree的插入

插入的过程分两步完成:

  1. 利用前述的B-树的查找算法查找关键字的插入位置。若找到,则说明该关键字已经存在,直接返回。否则查找操作必失败于某个最低层的非终端结点上。

  2. 判断该结点是否还有空位置。即判断该结点的关键字总数是否满足n<=m-1。若满足,则说明该结点还有空位置,直接把关键字k插入到该结点的合适位置上。若不满足,说明该结点己没有空位置,需要把结点分裂成两个。

分裂的方法是:
生成一新结点。把原结点上的关键字和k按升序排序后,从中间位置把关键字(不包括中间位置的关键字)分成两部分。左部分所含关键字放在旧结点中,右部分所含关键字放在新结点中,中间位置的关键字连同新结点的存储位置插入到父结点中。如果父结点的关键字个数也超过(m-1),则要再分裂,再往上插。直至这个过程传到根结点为止。

例子:

如果该节点的元素个数还没达到 m,则插入完后无需处理
比如:

如果该节点元素个数达到 m 时,这时候将元素插入到合适的位置,将最中间的元素取出,成为该节点的父节点元素,然后将其余左右元素拆成两个新节点

比如:

刚才的操作可能导致父节点的元素个数达到 m,这时候用情况 2 迭代处理,直到如果遇到根结点元素个数达到 m,则最中间元素将成为新的根结点。

比如:

B-Tree 的删除

我们需要分两种情况进行讨论:

  • 如果该元素存在于叶子结点,直接删除它,无需进行其它处理。
  • 如果该元素存在于非叶子节点,那么删除它将会留下一个空位,这时候我们需要一些处理来填充该位置。
    因为节点的元素个数在 [M/2, M] 的范围内,所以比如这里我们以 5 阶B-树为例,判断节点元素是否充足即满足个数则至少拥有三(2 + 1)个元素的节点才算是有充足的元素。

    1. 如果被删元素的左子树拥有足够的元素,这时候我们只需拿左子节点的最大值元素上来填充即可
    2. 当左子树不够元素而右子树元素充足时,这时候我们拿右子树的最小值元素上来进行填充
    3. 当左右子树所含元素均不足时,但左子树的左边兄弟节点的元素个数充足,这时我们需要拿左边的兄弟节点来进行调整。
    4. 当左右子树所含元素均不足时,但左子树的左边兄弟节点的元素个数也不足时,这时候我们还是拿左子树的最大值元素进行填充,之后再将该节点与其他节点合并形成新的节点。

B+Tree

B-Tree有许多变种,其中最常见的是B+Tree,例如MySQL就普遍使用B+Tree实现其索引结构。

与B-Tree相比,B+Tree有以下不同点:

  • 每个节点的指针上限为2d而不是2d+1。
  • B+Tree叶子节点保存了父节点的所有关键字和关键字记录的指针,每个叶子节点的关键字从小到大链接
  • 内节点不存储data,只存储key;叶子节点不存储指针。因此所有数据地址必须要到叶子节点才能获取到,所以每次数据查询的次数都一样。

索引

红黑树等数据结构也可以用来实现索引,但是文件系统及数据库系统普遍采用B-/+Tree作为索引结构,这一节将结合计算机组成原理相关知识讨论B-/+Tree作为索引的理论基础。

一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数

下面先介绍内存和磁盘存取原理,然后再结合这些原理分析B-/+Tree作为索引的效率。

磁盘存取原理

索引一般以文件形式存储在磁盘上,索引检索需要磁盘I/O操作。与主存不同,磁盘I/O存在机械运动耗费,因此磁盘I/O的时间消耗是巨大的。

下面是磁盘的整体结构示意图:

一个磁盘由大小相同且同轴的圆形盘片组成,磁盘可以转动(各个磁盘必须同步转动)。在磁盘的一侧有磁头支架,磁头支架固定了一组磁头,每个磁头负责存取一个磁盘的内容。磁头不能转动,但是可以沿磁盘半径方向运动(实际是斜切向运动),每个磁头同一时刻也必须是同轴的,即从正上方向下看,所有磁头任何时候都是重叠的(不过目前已经有多磁头独立技术,可不受此限制)。

下面是磁盘结构的示意图:

盘片被划分成一系列同心环,圆心是盘片中心,每个同心环叫做一个磁道,所有半径相同的磁道组成一个柱面。磁道被沿半径线划分成一个个小的段,每个段叫做一个扇区,每个扇区是磁盘的最小存储单元。为了简单起见,我们下面假设磁盘只有一个盘片和一个磁头。

当需要从磁盘读取数据时,系统会将数据逻辑地址传给磁盘,磁盘的控制电路按照寻址逻辑将逻辑地址翻译成物理地址,即确定要读的数据在哪个磁道,哪个扇区。为了读取这个扇区的数据,需要将磁头放到这个扇区上方,为了实现这一点,磁头需要移动对准相应磁道,这个过程叫做寻道,所耗费时间叫做寻道时间,然后磁盘旋转将目标扇区旋转到磁头下,这个过程耗费的时间叫做旋转时间。

局部性原理与磁盘预读

由于存储介质的特性,磁盘本身存取就比主存慢很多,再加上机械运动耗费,磁盘的存取速度往往是主存的几百分分之一,因此为了提高效率,要尽量减少磁盘I/O。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。

这样做的理论依据是计算机科学中著名的局部性原理:

当一个数据被用到时,其附近的数据也通常会马上被使用。

程序运行期间所需要的数据通常比较集中。

由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),因此对于具有局部性的程序来说,预读可以提高I/O效率。

预读的长度一般为页(page)的整倍数。页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页得大小通常为4k),主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,然后异常返回,程序继续运行。

B-/+Tree索引的性能分析

一般使用磁盘I/O次数评价索引结构的优劣。先从B-Tree分析,根据B-Tree的定义,可知检索一次最多需要访问h个节点。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。为了达到这个目的,在实际实现B-Tree还需要使用如下技巧:

每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个node只需一次I/O。

B-Tree中一次检索最多需要h-1次I/O(根节点常驻内存),渐进复杂度为\(O(h)=O(\log_d N)\)。一般实际应用中,出度d是非常大的数字,通常超过100,因此h非常小(通常不超过3)。

综上所述,用B-Tree作为索引结构效率是非常高的。

而红黑树这种结构,h明显要深的多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,所以红黑树的I/O渐进复杂度也为O(h),效率明显比B-Tree差很多。

上文还说过,B+Tree更适合外存索引,原因和内节点出度d有关。从上面分析可以看到,d越大索引的性能越好,而出度的上限取决于节点内key和data的大小:

\[
d_{max}=floor({pagesize \over keysize+datasize+pointsize})
\]

floor表示向下取整。

由于B+Tree内节点去掉了data域,因此可以拥有更大的出度,容纳更多的节点,能够有效减少磁盘IO次数

一般在数据库系统或文件系统中使用的B+Tree结构都在经典B+Tree的基础上进行了优化,增加了顺序访问指针。

如上图图所示,在B+Tree的每个叶子节点增加一个指向相邻叶子节点的指针,就形成了带有顺序访问指针的B+Tree。做这个优化的目的是为了提高区间访问的性能,例如图4中如果要查询key为从18到49的所有数据记录,当找到18后,只需顺着节点和指针顺序遍历就可以一次性访问到所有数据节点,极大提到了区间查询效率

综上所述:
B+Tree做索引的优势是:

  1. 内部节点取消data域,每一页可以容纳更多的数据,有效减少磁盘IO次数。
  2. 数据都存储在叶子节点,所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。所以B+树查询时间复杂度为log n,而B树查询时间复杂度不固定,与所查结点在树中的位置有关,最好为O(1)。
  3. 通过增加顺序访问指针提高区间查询效率。

而MongoDB索引选择B树可能是因为:
MongoDB 是文档型的数据库,是一种nosql,它使用BSON格式保存数据,归属于聚合型数据库。被设计用在数据模型简单,性能要求高的场合。之所以采用B树,是因为B树key和data域聚合在一起。因此并不需要类似于区间查询的操作。

参考文档

  1. MySQL索引背后的数据结构及算法原理
  2. 人人都是 DBA(VII)B 树和 B+ 树
  3. 平衡二叉树、B-Tree、B+Tree、B*树 理解其中一种你就都明白了
  4. https://zh.wikipedia.org/wiki/B%E6%A0%91
  5. B-Tree gif:https://www.cs.usfca.edu/~galles/visualization/BTree.html
  6. 6. 数据结构 - B 树

MongoDB 及 Mysql 背后的 B/B+树的更多相关文章

  1. 【译】MongoDb vs Mysql—以NodeJs为例

    亲爱的读者,您可能想知道为什么要写关于MongoDb和MySql这篇文章.那是因为我与NodeJs开发人员讨论在应用程序中使用哪种数据存储作为主要的数据存储方式. 我看过很多评论都在争论这个问题. 有 ...

  2. MongoDB与MySQL的插入、查询性能测试

    1.1  MongoDB的简单介绍 在当今的数据库市场上,MySQL无疑是占有一席之地的.作为一个开源的关系型数据库,MySQL被大量应用在各大网站后台中,承担着信息存储的重要作用.2009年,甲骨文 ...

  3. Mysql索引为什么用B+树而不用B-树

    先从数据结构的角度来看 我们知道B-树和B+树最重要的一个区别就是B+树只有叶节点存放数据,其余节点用来索引,而B-树是每个索引节点都会有Data域. 这就决定了B+树更适合用来存储外部数据,也就是所 ...

  4. 170504、MongoDB和MySQL对比(译)

    一.概要 几十年来,关系型数据库已经成为企业应用程序的基础,自从MySQL在1995年发布以来,它已经成为一种受欢迎并且廉价的选择.然而随着近年来数据量和数据的不断激增,非关系数据库技术如MongoD ...

  5. mongodb,redis,mysql的区别和具体应用场景

    一.MySQL 关系型数据库. 在不同的引擎上有不同 的存储方式. 查询语句是使用传统的sql语句,拥有较为成熟的体系,成熟度很高. 开源数据库的份额在不断增加,mysql的份额页在持续增长. 缺点就 ...

  6. 为什么mysql innodb索引是B+树数据结构

    1.文件很大,不可能全部存储在内存中,所以要存在磁盘上 2.索引的组织结构要尽量减少查找过程中磁盘I/O的存取次数(为什么用B-/+Tree,还跟磁盘存取原理有关) 3.B+树所有的data域在叶子节 ...

  7. MongoDB与MySQL的插入性能测试【转】

    1.1  MongoDB的简单介绍 在当今的数据库市场上,MySQL无疑是占有一席之地的.作为一个开源的关系型数据库,MySQL被大量应用在各大网站后台中,承担着信息存储的重要作用.2009年,甲骨文 ...

  8. MySQL 树形索引结构 B树 B+树

    MySQL 树形索引结构 B树 B+树   如何评估适合索引的数据结构 索引的本质是一种数据结构 内存只是临时存储,容量有限且容易丢失数据.因此我们需要将数据放在硬盘上. 在硬盘上进行查询时也就产生了 ...

  9. mongodb,redis,mysql的区别和具体应用场景(转)

    一.MySQL 关系型数据库. 在不同的引擎上有不同 的存储方式. 查询语句是使用传统的sql语句,拥有较为成熟的体系,成熟度很高. 开源数据库的份额在不断增加,mysql的份额页在持续增长. 缺点就 ...

随机推荐

  1. vue2.0 keep-alive 最佳实战(转载)

    1.基本用法 vue2.0提供了一个keep-alive组件用来缓存组件,避免多次加载相应的组件,减少性能消耗 <keep-alive> <component> <!-- ...

  2. 网页访问过程(基于CDN)

    1. 全局负载均衡(基于DNS) 如果有多台 WEB 服务器同时为一个域名提供服务时,即一条 URL 对应多个 IP 地址,那么该 URL 的权威域名服务器可能会根据该 URL 解析出多个 IP 地址 ...

  3. Sql Server外键约束

    一.添加约束(级联删除) 1.创建表结构时添加 create table UserDetails(id int identity(1,1) primary key,name varchar(50) n ...

  4. UVA11027_Palindromic Permutation

    此题不错.给你一些字字符,要求你用这些字符构成一个回文串,求字典序第k大的回文串. 首先通过给定的字符,我们可以直接判断能否构成回文串(奇数的字符不超过一种),其次可以统计每个字符在回文串的左边应该出 ...

  5. Powerful array CodeForces - 86D(莫队)

    给你n个数,m次询问,Ks为区间内s的数目,求区间[L,R]之间所有Ks*Ks*s的和.1<=n,m<=200000.1<=s<=10^6 #include <iostr ...

  6. MT【161】韦恩图

    (清华2017.4.29标准学术能力测试25) 若$N$的三个子集$A,B,C$满足$|A\cap B|=|B\cap C|=|C\cap A|=1$,且$A\cap B\cap C=\varnoth ...

  7. BZOJ3635谈笑风生

    一些闲话 这题方法好多啊QAQ,离线有BIT.长链剖分,在线有线段树合并,主席树等. 要我出题绝对不可能放离线过... 题面链接 权限题诶 洛谷 题意简述 简单的看一下题意,就是给定\(a\),求任何 ...

  8. 洛谷 P2915 [USACO08NOV]奶牛混合起来Mixed Up Cows 解题报告

    P2915 [USACO08NOV]奶牛混合起来Mixed Up Cows 题意: 给定一个长\(N\)的序列,求满足任意两个相邻元素之间的绝对值之差不超过\(K\)的这个序列的排列有多少个? 范围: ...

  9. IO编程(1)-文件读写

    文件读写 读写文件是最常见的IO操作.Python内置了读写文件的函数,用法和C是兼容的. 读写文件前,我们先必须了解一下,在磁盘上读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接 ...

  10. 第一章:CDib类库的建立

    VC++图像处理程序设计(第1版)    杨淑莹 编著     边奠英 主审 第一章 位图基础 Joanna-In-Hdu&Hust 手工打,印象更深刻 使用工具 VS2010 mfc  整本 ...