MySQL索引那些事

Posted 问北

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL索引那些事相关的知识,希望对你有一定的参考价值。

原文链接

大家有没有遇到过慢查询的情况,执行一条SQL需要几秒,甚至十几、几十秒的时间,这时候DBA就会建议你去把查询的 SQL 优化一下,怎么优化?你能想到的就是加索引吧?

 

为什么加索引就查的快了?这就要从索引的本质以及他的底层原理说起。

 

索引是什么

 

那索引到底是什么呢?你是不是还停留在大学学『数据库原理』时老师讲的“索引就像字典的目录”这样的概念?老师讲的没错,但没有深入去讲。

 

其实索引就是一种用于快速查找数据的数据结构,是帮助MySQL高效获取数据的排好序的数据结构。

 

索引的好处

 

举例说明索引的好处以及是怎么加快查询的。

 

假设我们有一个表t,它有俩字段,Col1和Col2,如下:

 

image.png

 

不加索引

不加索引的情况下,SQL: select * from t where t.col2=89 ,需要从表的第一行一行行遍历比对col2的值是否等于89,这样需要比对6次才能查到。这只是只有几行记录的表,那如果是百万级千万级的表呢?是不是就比较的次数就更多了,那还不得慢死。

 

加索引

如果col2这列加了索引,mysql内部会维护一个数据结构。假设mysql用的数据结构是红黑树(右子树的元素大于根节点,左子树的元素小于根节点)的数据结构建立索引,那就像上图右边那样。这样的话,刚才的那条SQL是不是只需要2次磁盘IO就查到了,是不是快很多了。

 

这就是索引的好处。索引使用比较巧妙的数据结构,利用数据结构的特性来大大减少查找遍历次数。

 

索引底层数据结构的探索

既然索引底层原理是利用一些巧妙的数据结构维护我们的数据,使得查询效率很高,那索引底层使用的什么数据结构呢?又是怎样来维护我们的数据呢?下面就带着大家一起探索一下索引的底层数据结构。

 

索引可选的数据结构 :

  • 二叉树
  • 红黑树
  • hash
  • B-tree

 

但mysql索引的底层用的并不是二叉树和红黑树。因为二叉树和红黑树在某些场景下都会暴露出一些弊端或者说缺点。

 

二叉树

 

我们看一下二叉树如果作为索引的底层数据结构在什么样的场景下有怎么样的缺点和不足。

 

假设把刚才的SQL改一下,用col1作为条件来查找,SQL: select * from t where t.col1 = 6 。

 

假如把col1作为索引,col1这列的数据特点是从上到下依次递增,类似于自增主键,那在每插入一行在维护二叉树这样一个数据结构的时候,我们看一下二叉树维护成什么样子了。

 

打开这个网址(国外的),可以演示数据结构维护的过程。依次插入1、2、3、4、5...

通过这个网站的演示插入这些数据,我们可以看到这样的一个二叉树是不是一直在单边增长,没有左子树。再仔细看一下和我们学过的链表是不是很像,也就是说二叉树在某些场景下退化成了链表。

 

二叉树插入演示.gif 

  image.png

链表的查找是不是需要从头部遍历啊,这时候和没加索引从表的第一行遍历是不是没什么太大区别?这就是mysql索引底层没有使用二叉树这种数据结构的原因之一。

 

当二叉树像上图一样退化成链表后,我们去查col1=6的记录是不是从二叉树的根节点依次遍历,遍历6次才能查到,和不加索引从表里一行行的遍历没太大差别。这是二叉树所谓索引底层数据结构的弊端之一。

 

红黑树

 

那有没有更好的数据结构用来存储索引,帮助我们更快的查找呢?比方说红黑树或hash表。

 

我们先看下红黑树。红黑树是什么?

是一种平衡二叉树,JDK1.8的HashMap就用到了红黑树。

 

那我们把刚才的一样的数据用红黑树来看一下是什么样的效果,同样打开刚才的网址,我们选择红黑树。

 

image.png

 

依次插入1、2、3、4、5、6、7看一下效果,可以看到,当有单边增长的趋势时红黑树会进行一个平衡(旋转)。这时,我们查询col1=6的数据时,查了3次,比二叉树又有了改进。红黑树演示.gif

 image.png

 

先告诉你mysql索引用的数据结构也不是红黑树,而是B+Tree(B-Tree的变种)。那为什么MySQL也没用红黑树做索引的数据结构呢?说白了红黑树还是有缺陷的。

 

红黑树做索引底层数据结构的缺陷

我们可以想一下,对于一些大公司特别是互联网公司,表数据动辄数百万数千万,那这样的表我们可以想象一下,现在我们只有7条记录,树的高度就达到了4层,那数百万数千万甚至上亿记录的表创建的索引它的树高得有多高?

 

假如说我查找的数据在底层的叶子节点上,一般来说都是从根节点开始查找,假如树的高度是50,那我要进行50次查找,50次磁盘IO那得多慢啊这开销已经很大了。这就是红黑树作为索引数据结构的弊端:树的高度过高导致查询效率变慢。

 

那能不能做一点改造呢?我们看,红黑树的树越高遍历次数会越多,会因为树的高度影响查询效率。所以我们要解决的问题就是减少树的高度,尽量控制它的高度在一个阈值范围内。假设说不大于5,即使数据达到1千万2千万最多也就5次磁盘IO就找到了,5次磁盘IO也是可以接受的毕竟表数据这么大嘛。

 

怎么改造能达到这个效果呢????想一下,既然树的高度不让增加,又想存很多数据。也就是说限制了纵向发展,那就横向发展呗。(身高已经增长不了了,长胖还是可以的)

 

对于上图的红黑树来说每个节点的子节点最多就2个,那基于横向增长的思想就让他变成3叉、4叉、5叉.....让子节点增加,让每一个高度可以存储更多的索引元素,每个节点又分叉,分出来的叉又有很多个节点。那么存储同等数量级别的数据,横向存储的越多,树高就越小了。这样的一个改造结果就是B-Tree。

 

Hash

待会儿有别的问题会引入hash。

 

B-Tree

  • 叶节点具有相同的深度,叶节点的指针为空
  • 所有索引元素不重复
  • 节点中的数据索引从左到右递增排序

 

image.png

 

就这样的一个结构。也就是说在一个节点上可以存储更多的元素,k-v,key就是索引字段,data就是索引字段所在的那一行的数据或是那一行数据坐在的的磁盘文件地址、指针,再去查找元素的时候一次性不是Load一个小元素,而是把一个大的节点的数据一次性全部load到内存,然后再在内存里再去比对,在内存里操作是比较快的。

 

如果我们要查找49这个元素,实际上是从根节点开始查找的,它一次性将根节点这个大节点一次性load到内存里,然后用要查找的元素在这里去比对,49大于15小于56,在15和56之间有一个节点存储的是下一个节点的磁盘地址指向下一个节点(这个节点的索引都是大于15小于56的),然后再将这个节点一次性load到内存去找这个元素,然后比对就找到了。

注意,一次load节点是一次磁盘IO,是非常慢的,但是我们把它load到内存中之后在你内存里随机的找某一个元素是非常快的,跟一次磁盘IO这个时间消耗去比对的话几乎可以忽略不计。

 

那按这种说法树的高度越小越好,那按这种思路可不可以把一个表的数据都放到一个大的节点上?然后把这个节点一次性load到内存里,我再在内存里一个个去比对不行吗?不是说内存里去比较查找元素是非常的快嘛,跟一次磁盘IO去比对快的多。不可以这样吗?

 

答案是否定的。

凡事都有个度。你想想,假如我们有几千万数据,在磁盘上面全部放到一个节点上去是不可能的,你的数据表是一行行插入的,存在磁盘上面几百兆甚至几个G,一次性load到内存中合适吗?内存本来就有限,一次性load这么大的数据,而且如果你学过计算机组成原理你也知道,磁盘IO跟内存打交道的单位是4K,一次可能读取4K的数据,可能有时候有一些局部读取的原理可能会取几十K(4的整数倍),取个16K,24K也是可以的 。但是一次交互取这么大是搞不定的,这是计算机组成原理定的,一次磁盘IO取那么多数据,对内存也是非常的浪费,而且这一次磁盘IO也是非常慢的。所以这个节点的大小设置要合适,不能太大也不能太小,mysql对这个节点大小设置的是16K,用下面这个SQL就是可以查到 show clobal status like \'Innodb_page_size\' 。

 

image.png

 

为啥设置16K?为什么不是更大的如16M呢,16K已经足够用了。等会儿会具体讲。

 

MySQL索引选择的不是原生的B-Tree,而是对他进行了改造,得到的是一种叫做B+Tree的数据结构

 

B+Tree(B-Tree变种)

  • 非叶子节点不存储data,只存储索引(冗余),可以放更多的索引
  • 叶子节点包含所有索引字段
  • 叶子节点用指针连接,提高区间访问的性能

 (实际上叶子节点间的指针是双向的,图有问题)

image.png

 

和B-Tree有啥区别?非叶子节点没有数据,数据都挪到叶子节点,叶子节点之间还有指针,非叶子节点之间跟原来一样没有指针。

 

为啥data元素挪到叶子节点?非叶子节点只存储索引元素,叶子节点存储了一份完整表的所有行的索引字段,data元素是每个索引元素对应要查找的行记录的位置或行数据,这样非叶子节点的每个节点就可以存储更多的索引元素(等会会有一个大致的估算)。实际上非叶子节点存储的是一些冗余索引,看一下上图,15/20/49,选择的是整张表的哪些数据作为索引?选择的是处于中间位置的,因为它要用到B+Tree一些比大小去查找,B+Tree本质可以叫做多叉平衡树,单看B+Tree的某一小块他还是一个二叉树。

image.png

 

还有一个特点,某一个节点的元素处于一个递增的顺序,会提取叶子节点的一些处于中间位置的数据作为冗余索引,查找的时候从根节点开始查找,先把根节点加载到内存里去,然后在内存里去比对。

image.png

 

比如要查找索引为30的数据,先在根节点跟15去比较,大于15,然后小于56,然后从他俩中间的指针查找下一个节点把它load到内存,再在内存里去比对,大于15,大于20,然后小于49,就根据20和49之间的指针找到下一个节点,然后load到内存,去比对,不等于20下一个30,相等,OK了。

为什么把中间的元素提取出来做冗余元素,为的是查找效率更高。

 

回到刚刚的问题,为啥要搞这些冗余索引,而且把这些冗余索引的data元素搞到叶子节点?也就是说B+Tree相对于B-Tree来说我的非叶子节点是不存储data元素的,叶子节点才存储data元素?

你想一下,一个节点不能太大也不能太小,就是16K,把data元素挪走以后,是不是这个节点就能存更多的冗余索引了,意味着分叉就更多了,意味着叶子节点就能存储更多的数据了。

 

mysql为什么把节点大小设置为16K,而不是更大?

假设索引字段类型是Bigint,8bit,每两个元素之间存的是下一个节点的地址,mysql分配的是6bit,也就是说一个索引后面配对一个节点地址,成对出现,可以算一下16K的节点可以存多少对也就是多少个索引,8b+6b=14b,16K /14b=1170个索引,叶子节点有索引有data元素,假设占1K,那一个节点就放16K/1K=16个元素,假设树高是3,所有节点都放满,能放多少数据?可以算一下,1170*1170*16=21902400,2千多万,mysql设置16K的大小,数据就可以存2千多万就已经足够了吧,既能保证一次磁盘IO不要Load太多的数据 又能保证一次load的性能,即便表的数据在几千万的数量也能保证树的高度在一个可控的范围。

 

可以看一下几千万的数据表是不是加了索引几十毫秒几百毫秒就出结果了,所以就解释了几千万的表精确的使用索引后他的性能依旧比较高。

 

树的高度只有3的情况下就能存储2千多万的数据,即便某一个索引在叶子节点,那也就2、3次磁盘IO就能查找到,当然很快了。而且mysql底层的索引他的根节点,是常驻内存的,直接就放到内存的,查找叶子节点,一个2千万的数据放到B+Tree上面,要查找叶子节点,就只需要2次磁盘IO就搞定了,在内存里比对的时间基本可以忽略。

 

MySQL是如何存储索引和数据的

 

刚才讲的原理性的比较多,现在结合具体的mysql的表不同的索引来看一下它底层到底是如何运用B+Tree来维护索引的。

索引和数据存放位置是哪?

首先问下mysql的表、数据、索引是放到那里的?

磁盘=》默认是安装目录的data文件里(不同版本可能有所不同),每个数据库对应data文件夹里的一个文件夹

 

MySQL索引那些事

一文搞懂底层mysql 索引那些事

一文搞懂底层mysql 索引那些事

MySQL和B+树的那些事&mysql 索引原理

MySQL索引那些事:什么是索引?为什么加索引就查得快了?

Mysql中join的那些事