MySQL索引结构为什么是B+树

索引是一种提高我们查询效率的数据结构,大家肯定很熟悉,在日常数据库优化工作中经常会接触到。

今天说一说索引的底层结构。

【索引结构】


MySQL 索引一般是哈希表或 B+ 树,常用的 InnoDB 引擎默认使用的是 B+ 树来作为索引的数据结构。

为什么不用哈希表?

什么是哈希表?

哈希表(也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

简单地说,哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需一次哈希算法即可立刻定位到相应的位置,速度非常快。

这里的对应关系f称为散列函数,又称为哈希(Hash函数),采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(Hash table)。

image

因为索引自身只需存储对应的哈希值,所以索引的结构十分紧凑,这也让哈希索引查找的速度非常快。然而,哈希索引也有他的限制:

  • 哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行,不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能的影响并不明显。
  • 哈希索引数据并不是按照索引值顺序存储的,所以也就无法用于排序
  • 哈希索引也不支持部分索引列匹配查找,因为哈希索引始终是使用索引列的全部内容来计算哈希值的。
  • 哈希索引只支持等值比较查询,包括=、IN()、<=>、也不支持任何范围查询。
  • 访问哈希索引的数据非常快,除非有很多哈希冲突(不同的索引列值却有相同的哈希值)。当出现哈希冲突的时候,存储引擎必须遍历链表中所有的行指针,逐行进行比较,直到找到所有符合条件的行。
  • 如果哈希冲突很多的话,一些索引维护操作的代价也会很高。例如,如果在某个选择性很低(哈希冲突很多)的列上建立哈希索引,那么当从表中删除一行时,存储引擎需要遍历对应哈希值的链表中的每一行,找到并删除对应的引用,冲突越多,代价越大。

因为这些限制,哈希索引只适用于某些特定的场合。如果使用 B+ 树作为索引数据结构,那么访问或修改一条数据的时间复杂度是 O(log n),但是使用哈希表作为索引结构干这些活的时候,时间复杂度 O(1)。如果只是查一条数据或者修改一条数据,用哈希表做索引肯定给力!但是一般业务系统不会这么简单。在我们业务开发中,不可能只操作一行数据。综合考虑,还是 B+ 树更适合作为索引的数据结构。在业务开发中,经常会遇到范围查询、排序查询等需求。这个时候哈希表索引就没办法高效的处理这些需求了。它只能通过扫表来实现这些功能,扫表是数据库的噩梦。MySQL 使用 B+ 树数据结构非叶子节点只储存键值,叶子节点会储存数据或者是主键。并且在叶子节点中键是按照顺序存储的,使得范围查询、排序查询等变得异常简单。

B+树索引和哈希索引的区别:

  • 如果是等值查询,那么哈希索引明显有绝对优势,因为只需要经过一次算法即可找到相应的键值;当然了,这个前提是,键值都是唯一的。如果键值不是唯一的,就需要先找到该键所在位置,然后再根据链表往后扫描,直到找到相应的数据;
  • 从示意图中也能看到,如果是范围查询检索,这时候哈希索引就毫无用武之地了,因为原先是有序的键值,经过哈希算法后,有可能变成不连续的了,就没办法再利用索引完成范围查询检索;
  • 同理,哈希索引也没办法利用索引完成排序,以及like ‘xxx%’ 这样的部分模糊查询(这种部分模糊查询,其实本质上也是范围查询);
  • 哈希索引也不支持多列联合索引的最左匹配规则;
  • B+树索引的关键字检索效率比较平均,不像B树那样波动幅度大,在有大量重复键值情况下,哈希索引的效率也是极低的,因为存在所谓的哈希碰撞问题。

为什么不用 B 树?

什么是B树?又叫做B- 树(其实B-是由B-tree翻译过来,所以B-树和B树是一个概念) ,它就是一种平衡多路查找树。下图就是一个典型的B树:

image

为了更好的描述B树,我们定义记录为一个二元组[key, data],key为记录的键值,data表示其它数据(上图中只有key,没有画出data数据 )。
下面是对B树的一个详细定义:

image
  • 有一个根节点,根节点只有一个记录和两个孩子或者根节点为空;
  • 每个节点记录中的key和指针相互间隔,指针指向孩子节点;
  • d是表示树的宽度,除叶子节点之外,其它每个节点有[d/2,d-1]条记录,并且些记录中的key都是从左到右按大小排列的,有[d/2+1,d]个孩子;
  • 在一个节点中,第n个子树中的所有key,小于这个节点中第n个key,大于第n-1个key,比如上图中B节点的第2个子节点E中的所有key都小于B中的第2个key 9,大于第1个key 3;
  • 所有的叶子节点必须在同一层次,也就是它们具有相同的深度;
    由于B-Tree的特性,在B-Tree中按key检索数据的算法非常直观:首先从根节点进行二分查找,如果找到则返回对应节点的data,否则对相应区间的指针指向的节点递归进行查找,直到找到节点或找到null指针,前者查找成功,后者查找失败。

B+ 树的非叶子节点上只储存键值,而 B 树的非叶子节点上不仅储存键值还储存数据。在 MySQL 数据库中数据页的大小是固定的,Innodb 引擎数据页默认大小为 16 KB。B+ 树这种做法是为了让树的阶数更大,让树更矮胖。进行查询的时候,磁盘 IO 次数就会减少,查询效率也会更快。B+ 树的所有数据均储存在叶子节点中,并且是按键值有序排列。但是 B 树的数据分散在各个节点。进行范围查询,排序查询的时候,B 树的效率肯定不如 B+ 树。

B树和B+树的区别:

  • B+树中只有叶子节点会带有指向记录的指针(ROWID),而B树则所有节点都带有,在内部节点出现的索引项不会再出现在叶子节点中。
  • B+树中所有叶子节点都是通过指针连接在一起,而B树不会。

为什么是B+树B+树是B树的变种,是基于B树来改进的。为什么B+树会比B树更加优秀呢?

B树:有序数组+平衡多叉树;
B+树:有序数组链表+平衡多叉树;
B+树的关键字全部存放在叶子节点中,非叶子节点用来做索引,而叶子节点中有一个指针指向一下个叶子节点。做这个优化的目的是为了提高区间访问的性能。而正是这个特性决定了B+树更适合用来存储外部数据。

B+ 树查找过程

image

磁盘块 1 中存储 17 和 35 数据项,还有 P1、P2、P3 指针,P1 表示数据项小于 17 的磁盘块,P2 表示数据项在 17 和 35 之间的数据项,P3 表示数据项大于 35 的数据项。非叶子节点不储存数据,只储存指引搜索方向的数据项。我们知道每次 IO 读取一个数据页的大小,也就是一个磁盘块。假设我们要查找 29 这个数据项,首先进行第一次 IO 将磁盘块 1 读进内存,发现17 < 29 < 35,然后选用 P2 指针进行第二次 IO 将磁盘块 3 读进内存,发现26 < 29 < 30,然后选用 P2 指针将磁盘块 8 读进内存,在内存中做二分查找,找到 29,结束查询。通过分析查询过程,我们可以知道 IO 次数和 B+ 树的高度成正比。H 为树的高度,M 为每个磁盘块的数据项个数,N 为数据项总数。

从下面的公式可以看出如果数据量N一定,M越大相应的H越小。

image
image

M 等于磁盘块的大小除以数据项大小,由于磁盘块大小一般是固定的,所以减小数据项大小才能使得 M 更大从而让树更矮胖。这也是为什么 B+ 树把真实数据放在叶子节点而不是非叶子节点的原因。如果真实数据放在非叶子结点,磁盘块存储的数据项会大幅度减少,树就会增高相应查询数据时的 IO 次数就会变多。

B+ 树一般能储存多少数据?

这里我们先假设 B+ 树高为 2,即存在一个根节点和若干个叶子节点,假设一行记录的数据大小为 1 KB,那么单个叶子节点(页)中的记录数等于 16 KB / 1 KB = 16 条数据。然后要计算出非叶子节点能存放多少指针,我们假设主键 ID 为 bigint 类型,长度为 8 字节,而指针大小在 InnoDB 源码中设置为 6 字节,这样一共 14 字节,我们一个页中能存放多少这样的单元,其实就代表有多少指针,即 16 KB / 14 B = 1170。那么可以算出一棵高度为 2 的 B+ 树,大概就能存放下 1170 * 16 = 18720 条数据。根据同样的原理我们可以算出一个高度为 3 的 B+ 树就可以存放下 21902400 条数据。所以在 InnoDB 中 B+ 树高度一般为 1 - 3 层,它就能满足千万级的数据存储。在查找数据时一次页的查找代表一次 IO,所以通过主键索引查询通常只需要 1 - 3 次逻辑 IO 操作即可查找到数据。

【后记】


其实数据库索引调优是一项技术活,不能仅仅靠理论,因为实际情况千变万化,而且MySQL本身存在很复杂的机制,如查询优化策略和各种引擎的实现差异等都会使情况变得更加复杂。但同时这些理论是索引调优的基础,只有在明白理论的基础上,才能对调优策略进行合理推断并了解其背后的机制,然后结合实践中不断的实验和摸索,从而真正达到高效使用MySQL索引的目的。另外,MySQL索引及其优化涵盖范围非常广,本文只是涉及到其中一部分。如果有机会,希望再对本文未涉及的部分进行补充吧。

你可能感兴趣的:(MySQL索引结构为什么是B+树)