翡翠谷 2020-01-28
项目中一直使用Mysql,对于慢sql优化也一直在做,但是一直没有梳理清楚,这里简单总结一下
首先看一下mysql为什么要使用索引
1)索引是帮助Mysql高效获取数据的 排好序的 数据结构
2)索引存储在文件里
首先说明一下,Mysql是使用B+树作为索引的
在没有索引的情况下,如果要找到一条记录的化,是通过全表扫描的
一张数据表中记录了分数,有两个字段,id,core:
如果要查找core=5 的记录,where core=5 ,按照全表扫描顺序查找从第一条记录开始,需要查询6次才可以找到
如果我们对core字段加上索引后,假设使用的是最简单的二叉树作为索引的存储方式,同样的如果查找 where core =5 的记录,按照二叉树的存储结构,如下图
那么我们查找3次就可以找到了
那为什么Mysql中不使用二叉树作为索引呢,那就来看看二叉树的特点
(一)二叉树
二叉树是一种比顺序结构更加高效的查找目标元素的结构,从父节点开始,和目标元素比较,如果相等返回当前节点,如果目标元素值小于当前节点,则移动到当前节点的左侧子节点比较,如果目标元素值大于当前节点,则移动到当前节点的右侧子节点比较,反复操作,最终找到目标元素节点的位置后返回,二叉树的查找时间复杂度可以达到O(log2(n))。
但是二叉树有一个致命的缺点,如果我们的目标元素是有规律递增或者递减的,这样出来的二叉树就会失去平衡,变成了一个线性链表结构,比如使用二叉树作为索引,那么我们将数据库主键ID这一列建立索引,得到的索引的数据结构就变成下面这样,这种情况下,查找数据和没有索引是一样的效果,那么索引就起不到作用了。
针对上面的这种场景,我们很容易想到那为什么不用可以保持平衡的二叉树呢,比如说红黑树,那么我们就来看看红黑树的特点
(二)红黑树
红黑树是一种平衡二叉树,它继承了二叉树的优点,由解决了二叉树遇到的自增数据索引失效的问题,因为红黑树的会对树的结构进行调整,进行左旋或者右旋及颜色变换等操作,始终保证 左子节点数<父节点数<右子节点数
红黑树不作为Mysql索引的原因是,当数据量大 时候,树的深度也很大,每个父节点最多只能有两个子节点,如果表中有很多数据,那么树的深度很大,达到十几或几十层,后续会了解到这样的深度对于Mysql的磁盘寻址非常不利,也是相当耗时的。
那问题来了,如果是因为树的深度太高,查找数据不方便,那为什么不用可以直接定位到某条数据的hash算法作为Mysql的索引呢,那么我们来看看Hash的特点
(三)Hash算法
对数据进行hash,也就是散列运算,然后将hash结果作为文件指针,可以从索引文件中获得数据的文件指针,再到数据文件中获取到数据,按照这种结构,我们很快能定位出来某一条数据的位置,查询的效率非常高,那么它的问题点在哪呢,那就是不支持范围查询,分页或者大于 小于某个范围的查询都是无法支持的,只能支持固定的 字段名 = 目标值的场景,同样也不适合Like这种模糊查询,所以这种算法肯定是不适合作为数据库的索引的。
那我们再来看看平衡二叉树的问题
平衡二叉树的要求是 节点的子节点高度差不能超过1,如上图中节点20,左节点高度为1,右节点高度为0,高度差为1,所以上图可以定义为一个平衡二叉树,保证二叉树平衡的方法就是通过左旋或者右旋等操作
上图中保存的id的索引,如果要查找id=8的数据,首先要把根节点加载到内存中,然后进行比较,8比根结点的10小,所以要加载10的左节点就是5对应的磁盘块2,把5加载到内存中进行比较,8和5比较,需要加载5的右节点也就是8的索引对应的数据,发现命中,整个过程中进行了3次IO。
那么现在来看看怎么找到索引对应的数据呢
索引保存数据的方式一般有两种,第一种是在节点的数据区保存ID=8的行数据的所有数据具体内容,另一种方式是在节点的数据区保存的是真正保存数据的磁盘地址。
平衡二叉树解决了线性链表的问题,查询效率也还可以,但是Mysql不选用这种结构作为索引的原因总结一下:
1)搜索效率不足,在树的结构中,树的深度决定了搜索的IO次数,上面的查找id=8的数据,就进行了3次IO,当数据量达到百万级别的时候,树的高度导致的IO次数就很恐怖了。
2)查询的效率不稳定,如果查找的数据正好落在根节点,那么只需要一次IO,如果是在叶子节点,那么就需要多次IO。
3)节点存储的数据内容太少,没有很好的利用操作系统和磁盘数据交换特性,也没有很好的利用磁盘IO的预读能力。操作系统和磁盘之间一次数据交换是以页为单位的,一页 = 4K,也就是每次IO交互操作系统会将4K的数据加载到内存中,但是在二叉树的每个节点的结构中只保存了一个关键字,一个数据区,两个子节点的引用,并不能够填满4K的数据量,也就是辛辛苦苦做了一次IO操作,却只加载了一个关键字,在数的高度很高,搜索的数据又是在叶子节点,取一个关键字需要做很多次的IO
有没有办法解决这种问题呢?
有的,一种多路平衡查找树(Balance Tree),B Tree是一个绝对平衡树,所有的叶子节点在同一个高度
看一下定义,上面的这个树是一个 2-3树(每个节点存储2个关键字,有3路),每个节点保存的关键字个数和路数关系为:关键字个数=路数-1
假设从上图中查找id=28的数据,B Tree的查找过程是:首先把根结点加载到内存中,加载了17和35两个关键字, 比较,如果X<17,则指向P1,如果X=17,命中返回,如果 17<X<35,则指向P2,如果X=35,则命中返回,如果X>35,则指向P3,按照如上规则,命中28后,就去加载28对应的数据,找到28对应 数据区,数据区中存储的是具体的数据或者是指向数据的指针。
这种数据结构能够很好的利用操作系统和磁盘的交互特性,Mysql为了能更好的利用磁盘的预读能力,将页的大小设置为16K,就是将一个节点(磁盘块)的大小设置为16K,一次IO将一个节点(16K)内容加载到内存中,假设关键字的类型是int,4个字节,每个关键字对应的数据区也为4个字节,不考虑子节点应用的情况下,上图中每个节点大约能够存储(16*1000)/8 = 2000个关键字,那么对应的就是2001个路数,对于这种有2001个路数的B树,三层的高度能够搜索到的关键字的个数是远远大于普通的二叉树的
在B树保持树的平衡的过程中,每次关键字的变化都会导致机构发生很大的变化,这个过程是特别浪费时间的,所以创建索引一定要创建合适的索引,而不是把所有的字段都建立索引,创建冗余的索引只会对数据的增加、删除、修改增加更大的性能消耗。
既然B树已经能很好的解决问题了,为什么还需要用B+树呢?
首先来看一下B+树的概念
B+树是B树的一个变种,它不再遵循 关键字个数=路数-1 这个规则,数据的检索规则是采用的左闭合取件,路数和关键字的个数关系为1:1,看下图:
如果查找id=1的数据,搜索的规则是 1<= X <28 ,找到P1, 28<=X <66 ,找到P2, 66<=X,找到P3,最终是在叶子节点 节点1的数据区中获取真正的数据。
总结一下B树和B+树的区别:
1、B+树的关键字的搜索采用的是左闭合区间,之所以要这样是因为要最好的去支持自增的id,也是Mqsql设计的初衷,保证id=1命中的情况下,也去继续往下查找,知道找到叶子节点中1
2、B+树中的根结点和支节点中没有数据区,关键字对应的数据只保存在叶子节点中,所以只有叶子节点中的关键字数据区才会保存真正的数据内容或者数据对应的地址,但是在B树中,如果根结点命中,是直接返回的,B+树中,叶子节点不会保存子节点的引用
3、B+树的叶子节点是顺序排列的,并且相邻节点之间是顺序引用的关系,叶子节点之间通过指针相连
Mysql为什么最终选择了B+树作为索引的数据结构
1.B树能解决的问题,B+树都能解决,且能够更好的解决,降低了树的高度,增加节点的数据存储量。
2.B+树的扫库和扫表能力更强,如果根据索引去根据数据表扫描,对B树扫描,需要整颗树遍历,B+树只需要遍历所有的叶子节点
3.B+树的磁盘读写能力更强,根结点和支节点不保存数据区,所有的根结点和支节点天同样大小的情况下,保存的关键字更多,叶子结点不存子节点的引用,所以,B+树读写一次磁盘加载的关键字更多
4.B+树具有天然的排序功能
5.B+树的查询效率更加稳定,每次查询数据,查询IO次数是稳定的。