面试常见问题(必知必会)之数据库索引(一)索引的本质解析(索引存储为什么选择B+树不选择二叉树和红黑树)

  • 索引是帮助MySQL高效获取数据的排好序数据结构
  • 索引数据结构:
    • 二叉树
    • 红黑树
    • Hash表
    • B-Tree

在这里插入图片描述

如上图所示,先看左面我们看到了一个表。假如我现在需要查一条数据(select * from t where t.Col2=89),如果没有索引直接查询的话,它会从第一行数据依次执行I/O操作(耗费性能)读取每行数据进行比较,直至找到我需要的那行数据。(如果需要查找的数据在表的最后,就很耗时)
 
假设现在我将col2列建了索引,假设我在底层用二叉树这种数据结构来存储索引(如上图右边所示)。那么,现在我再次查找这条数据(select * from t where t.Col2=89),就先判断这个字段在表中是否是索引,如果是,那就先去索引对应的数据结构里面快速定位索引,然后再去查找对应的那一行数据。
 
上面提到了直接定位索引,然后直接去对应行查找数据。那么,索引是怎么存储的呢?图中二叉树就是索引,首先,先看它的每一个节点。它的每一个节点类似于一个key-value结构(key就是索引元素,value就是索引所在行的磁盘文件地址指针)。这样的话,我们就可以通过二叉树的特性,左边的值小于右边的值,很快找到89对应的索引,进而找到索引对应的磁盘文件地址指针,通过指针我们就可以直接定位那一行数据,就完成了查询。(我们可以看出二叉树比直接查询最少提高一倍速度甚至更多)
 
但是,我们知道实际上索引没有用二叉树。那是因为在某些场景,它是不好的。比如,上图中col1中的数据。我们直接来看一下它的结构。

在这里插入图片描述

如果现在我查找(select * from t where t.col1=6;),看上图我们的二叉树其实已经可以看成是链表。那么,它进行查找的话从根节点开始它需要查找6次,直接表查找也是6次。发现并没有什么优化。所以说用二叉树是不合适的。

二叉树不合适做索引,就会尝试用新的数据结构来解决对应问题,于是便想到用红黑树来存储索引。我们先看一下同样是上面col1的数据,红黑树是一个什么结构。

在这里插入图片描述
 
它如果一边太深,它就会自旋一下,让树两边差距不大。所以它也叫二叉平衡树。假设用它来做索引就能解决上面我们所遇到的问题。但是,我们都知道实际上索引使用的数据结构是B+树,那么红黑树又有哪些弊端呢?
 
实际上,我们数据库中要存储的数据根本不止上面那7行数据。动辄几十万行几百万行甚至更多。如果用红黑树来进行存储,树的高度就会很多。假设,我现在将一个表的数据用红黑树进行存储,它的高度是20的话,如果要查找的数据正好在叶子节点,那么我们就需要查找20次来查找对应的索引。所以说,随着数据量的不断变大,树的高度也在不断变大,查找的复杂度也在不断变。它是一个不可控的,况且它每查找跳跃一下就要执行一次I/O操作,我们都知道I/O操作是比较耗时的,所以我们尽量避免过多的进行I/O操作。这也就是不使用红黑树的原因。

 
从上文我们知道,我们之所以不选红黑树做存储索引的数据结构,是因为大量的数据量使红黑树的高度不断增大,导致查找变慢。所以,我们就在想如果可以改造一下红黑树,让它的高度一直小于等于3,那么它就算需要查找的数据在叶子节点也只需要查找3次就可以了。那么,可以怎么改造呢?
 
如果要树的高度不超过3,其实就是说我们不能纵向继续存储索引。那我们就只能横向存储多个索引来减少纵向存储,进而达到降低树的高度。想要横向存储足够多的索引,又想保证索引的快速查找,B树就被引入了。下面,我们先看一下B树的结构:

在这里插入图片描述

 
如上图所示,B树它会分配一个很大节点,这个节点横向可以存储很多的索引。且每一个索引之前又有一个指针会指向下一个节点的索引。除此之外,B树中叶节点具有相同的深度,叶节点的指针为空,所有索引元素也不重复,并且节点中的数据索引从左到右递增排序。
 
但实际上,数据库中的索引也不是用B树进行存储,而是对它进行改造,成为B+树来进行存储的。那么,B+树又长什么样呢?
 
如下图所示,是改造后的B+树,它与B树有什么区别呢?从上图(B树结构图)我们可以看出,在B树中非叶子节点有key-value,其中的value(data)就是我们之前提到的索引所在行的磁盘空间地址。那么,我们再看下图(B+树结构图),它将所有非叶子节点的data(也就是索引所在行的磁盘空间地址)都迁移到了叶子节点。第二点就是B+树的叶子节点之间它有双向指针相关联,但B树是没有的。

在这里插入图片描述
 

B+树的特点:
  • 非叶子节点不存储data,只存储索引(冗余),可以放更多索引。
  • 叶子节点包含所有索引字段。
  • 叶子节点用指针连接,提高区间访问的性能。
  • 每一层从左到右是依次递增的。
     
B+树的查询查找过程:

我们接着看上图。假设,我们要查找30所在行的磁盘空间地址。我们依旧是从根节点开始,先把根节点直接提取到内存中。因为B+数每一行的索引是从左到右依次递增的。我们就可以快速找到30所在的子节点的指针(也就是15和56之间)。(取到内存中去是因为在内存中直接查找比进行I/O操作会快很多很多(简直不能相提并论)),紧接着,我们会取出15到56直接的子节点(也就是我们刚刚找到的指针指向的子节点),然后继续查找30所在的子节点的指针(20到49之间)。找到以后,继续取出子节点,然后查找30,获取对应的data(30所在行的磁盘空间地址)。在这个过程中,我们只进行了三次查找跳跃(就是执行I/O操作)。

到了这儿,就有人会说。如果将所有索引都放到根节点,然后直接加载到内存中进行查找。先不说查过需要耗费多少时间,如果一个千万级数据表,加载一次所有索引那耗费的时间可想而知(不现实)。那么,一个节点中到点可以放多少索引呢?将上图的高度为3的B+树放满到底可以存储多少个索引。我们来计算一下。
 
其实,在数据库中每个节点都是用数据库的一页来进行存储。我们先看一下一页是多少字节(SHOW GLOBAL STATUS like “Innodb_page_size”;),我们可以查到一页是16384个字节。从上图中我们知道每个节点它是存储着n哥索引和它相对应的子节点的磁盘空间地址。索引大小是8个字节,磁盘空间地址为6个字节。那么每个节点就可以存储1170个索引。但是,子节点他的data不一定是磁盘空间地址,有可能是别的字段的值。因为不可控,那就给它一个较大的字节,假设它是1000字节。那么一张表大概可以存储16个索引及对应data。我们就可以计算出上图那个高度为3的B+树可以存储$ 1170 * 1170 * 16 = 21902400 $条数据。所以,我们只需要三次就可以快速查找到两千万级以内数据库的数据。
 
除此之外,我们一般还会将根节点直接常驻内存来提升查找速度。甚至会将第二层的节点也常驻内存,进一步提升查找速度(千万级数据)。

有人问超过两千万怎么办?再加一层呗。再加一层就足够放256亿的数据了。况且,一般到了那么大的数据,早去进行分库分表了。

 
 

面试题:
  • 为什么数据库索引使用B+树,而不使用二叉树和红黑树?
  • B+树和B树在底层索引存储区别。
  • 为什么用B+树存储索引会特别快?

猜你喜欢

转载自blog.csdn.net/qq_42546127/article/details/115215077