在这篇文章中,我会先介绍一下什么是索引,索引有什么做用。数据库
以后会介绍一下索引的数据结构是什么样的,有什么优势,又会带来什么样的问题。缓存
在分析完数据结构后,咱们能够根据这个数据结构,研究索引的用法,以及如何设计更高效的缓存。bash
最后,我会对上一篇的内容进行补充,介绍change buffer
的做用以及分析change buffer
对性能的影响。数据结构
在咱们学习索引以前,咱们要先了解它是什么,以及有什么做用。性能
官方对于索引的定义是这样的:学习
Indexes are used to find rows with specific column values quickly. Without an index, MySQL must begin with the first row and then read through the entire table to find the relevant rows.优化
也就是说,索引是用来快速查找具备特定值的一行数据(的一种数据结构)。若是没有索引,MySQL必须得从第一行开始逐行扫描数据。ui
尤为是当咱们的数据量愈来愈大的时候,恰当的索引是能够帮助咱们拥有更优秀的性能的。spa
这句话的另一层含义在于:若是索引设计的很差,可能会使得咱们的数据库性能变得更加的糟糕。设计
那么,索引究竟是什么呢?咱们接着往下看。
在讲索引具体的数据结构以前,咱们来想象一下咱们在英文词典里面找一个单词。
若是咱们须要找一个单词:"awesome"!
咱们会在目录里面找到以字母 A 开头的一系列单词,而后从以字母 A 开头的一系列单词中找到 W ,而后是 E ...
就这样不断的往下查找,不断缩小咱们的查找范围。若是咱们不适用目录,直接在正文里面找这个单词,可能须要花费更多的时间。
何况,这个词典里面的单词是排好序的,若是咱们找 Z 开头的字母,可能得找好几百页,才能最终找到。
这个例子不能说特别的准确,可是反映了索引的核心:减小查找的次数。
咱们都知道,MySQL的数据保存在了磁盘中。而磁盘的IO是最慢的。因此,减小磁盘的读写是提升性能必不可少的作法。虽然如今大多数计算机已经使用了SSD,再也不须要寻道等,可是索引的原则仍是成立的。
这里咱们来看看InnoDB的B+树是怎么实现的(图来自于《高性能MySQL》):
能够看出,这是一颗N叉树,树中的每个结点,都是MySQL中的一个数据页。
其实说白了这里的N叉树,和二叉查找树查找逻辑是同样的。只不过不一样的地方在于这里的每个结点,包含了比二叉查找树更多的数据与指针。这样作的目的是使得在数据量相同的状况下,B+树可使得树的高度更低。
而又由于全部的数据页都是持久化保存在磁盘中的,因此更低的高度意味着查找一个数据须要进行磁盘IO的次数越少,效率变得更高。
注意,由于N叉树的N越大,对应的树的高度就会越低。而每个结点(每个数据页)的大小是固定的(默认是16K,可使用innodb_page_size
参数修改),因此当设置为索引的key越小的时候,N就会越大。
在通过上面的介绍以后,我想你应该能理解索引的查找方法了。下面咱们再来讲说索引的分类:
主键索引和非主键索引。
主键索引,就是非叶子结点中存储的值都是主键的值,在查找的时候经过主键查找。直到查找到最后的叶子节点。在最后的叶子节点中保存了这个主键对应的整行数据。
非主键索引,就是非叶子结点中存储的值都是索引的值,查找的时候经过这一个数值进行查找。查找到最后的叶子节点,保存了对应的主键ID。而后,MySQL会根据查到的主键,再查找主键索引对应的B+树,直到找到这一行的全部数据。而这个经过查找到主键,而后再利用主键来再次查找,或者这一行数据的过程,称为回表。
注意,咱们在新建一张表的时候,必定会有一颗以主键为索引的B+树。哪怕你没有设置主键,MySQL都会选一个不包含NULL的第一个惟一索引列做为主键列,并把它用做一个主键索引。若是没有这样的索引就会使用行号生成一个汇集索引,把它当作主键。
此外,每增长一个索引,MySQL就会多维护一颗B+树。维护B+树的过程也是很复杂的,涉及到了页的分裂等,我想在之后的文章进行介绍。
另外以前也提到了,影响MySQL性能的一个很重要的因素就是磁盘IO。而回表这个操做,无异于增长了不少的IO次数。
那么有什么办法能够减小这一部分的开销吗,咱们接着往下看。
咱们在上面提到的索引,都是单个的数据进行查找。
这样的话,咱们每次对其中一个列创建一个索引,就得多维护一颗B+树,一样对性能和空间形成了浪费。
那么咱们有没有可能同时对多个数据进行排序,而后再进行查找呢?答案是能够的,咱们能够采用联合索引。
以上面这张图为例:
咱们创建了一个(姓名,年龄)的联合索引。
若是咱们须要找一个15岁的法外狂徒(误)张三:
select * from user where name = "张三" and age = 15;
复制代码
由于此时咱们的查找条件彻底匹配了咱们定义的索引,因此MySQL会先从查找的第一个条件开始,找到名为“张三”的数据,而后此时会继续判断第二个年龄为15岁的条件,由于此时大于第一个数据项中的10岁,且小于第二个数据项中的20岁,因此会从第二个指针往下寻找,查找大于10岁且小于20岁的“张三”。
这种条件和索引彻底匹配的查找过程,称为全值匹配查询。
可是,假设咱们没有设置多个查找条件,只搜索名字为“张三”的人。
select * from user where name = "张三";
复制代码
那么此时的查找过程不会去匹配年龄这一列,只会比较姓名这一列。因此,会从这颗B+树最左边的结点,8岁的张三
开始,不断的向后遍历,直到这个数据的姓名不叫“张三”为止。
这样的查找过程,称为最左前缀查找。简单的来解释,就是查找的条件只要是符合这个联合索引,或者符合这个联合索引的最左边几项,索引就会生效,也就实现了“剪枝”的目的,加速了查找的速度。只有剩下的那些不符合最左前缀的条件,才会依次遍从来进行匹配。
也就是说,只要知足最左前缀,就能够利用索引来加速检索。这个最左前缀能够是联合索引的最左N个字段,也能够是字符串索引的最左M个字符。
那么,何时最左前缀不会生效呢?
假设有这么一个联合索引(a, b, c, d, e, f)。那么查找条件是(a)、(a, b)、(a, b, c)等都是称为符合最左前缀的。也就是说,必定要从索引的最左边开始,任意N个字段或者M个字符。
可是若是咱们使用了(a, c, d)这样的查找条件,那么只会对(a)起做用,(c, d)是不会生效的。由于最左前缀被中断了。
而若是是(e, f)这样的查找条件,也一样不会生效,由于也不符合最左边的N个字段的规则,不属于最左前缀。
因此,索引的复用能力是咱们在创建联合索引时候的一个评估标准。由于能够支持最左前缀,因此当已经有了(a,b)这个联合索引后,通常就不须要单独在a上创建索引了。所以,第一原则是,若是经过调整顺序,能够少维护一个索引,那么这个顺序每每就是须要优先考虑采用的。
可是,联合索引也不是越长越好。咱们在前面提到过,要尽量的让N叉树的N比较大,这样树的高度会比较低,以此来减小磁盘的IO次数。若是联合索引包含的字段比较多,在页面大小固定的状况下,会形成N值的减小,反而会减慢效率。
继续上面的法外狂徒的例子。
假设咱们的语句是这样的:
select * from user where name like "张%" and age = 15;
复制代码
很好理解,咱们会以为MySQL会从名字以“张”开头的数据开始遍历,而后判断年龄是否为15。
可是最左前缀有一个很是重要的原则:MySQL会一直向右匹配直到遇到范围查询(>、<、between、like)就中止匹配。
也就是说,此时咱们的查询,age
这个索引是用不上的。
因此,在MySQL5.6以前,只要找到了符合以“张”开头的名字这个条件,就会经过这个数据的主键ID,进行回表的操做,而后查找这个数据的年龄是否为15。
而MySQL 5.6 引入的索引下推优化(index condition pushdown), 能够在索引遍历过程当中,对索引中包含的字段先作判断,直接过滤掉不知足条件的记录,减小回表次数。也就是说,直到找到了以“张”开头的名字而且年龄为15,才会进行回表。
此外,在回表以前,若是使用了Multi-Range Read (MRR)
这个策略,在取出主键后,回表以前,会在对全部获取到的主键排序。
还记得咱们前面说到的吗,若是咱们采用的是非主键索引,那么咱们查到了这个数据以后,还须要根据叶子节点中的主键,再回表一次。
覆盖索引能够解决这个问题。好比咱们前面查找“张三”的时候,咱们也能够同时找到他的年龄。好比(a,b)这样的联合索引,在咱们使用
select b form table_name where a = xxx;
复制代码
这么一条语句的时候,找到了符合条件的a,不须要经过主键来进行回表,找到b的值,而是会直接返回记录在这颗B+树中的值。也就是说,在这个查询里面,索引(a,b)已经“覆盖了”咱们的查询需求,咱们称为覆盖索引。
咱们先来分析查询方面的性能。
对于查询来讲,若是这个是普通索引,那么在找到了符合条件的数据以后,会日后继续遍历,直到碰到不知足的数据为止。
若是是惟一索引,因为他的惟一性,只要找到了,那就直接返回就行,不须要继续日后遍历。
其实二者的性能差距微乎其微。
为何呢?你可能会想:普通索引还须要继续遍历,有可能会更慢。可是,咱们以前提到过,查询操做是须要把数据读到内存的,而且是以数据页的形式读到内存。而在内存中的遍历操做,速度方面的差距是特别小的。
就算普通索引的最后一项仍是相同的,须要经过磁盘IO来读取下一页,这个时候多是比较耗费时间的。不过由于一个数据页包含了特别多的数据,这种可能性是特别低的。
在咱们说到插入以前,我先要跟你介绍一下change buffer
这个东西。
我在上一篇文章中提到:在咱们须要更新数据的时候,先把数据从磁盘读到内存中,修改这个数据,而后修改redolog
,增长binlog
,等内存满了以后或者redolog写满了以后,再将脏页刷回磁盘。
那么插入数据呢?
在咱们新增了一条数据以后,MySQL并不会将这个插入直接写入磁盘中,而是会将这个修改写入change buffer
中。
在以后有关于这个数据页的查询请求的时候,才会读取这一个数据页,而后根据change buffer
中关于这一页的记录,依次更新到读取到了内存中的数据页中,这个过程称为merge。在更新完毕以后,才把查询结果返回。
可是这样有什么用呢?
假设咱们插入的普通索引不在内存中,此时有两个做用:
第一,由于咱们在插入一条数据的时候,不须要经过磁盘的IO把须要写入的数据页调入内存中进行修改,而是会将这个插入行为记录下来,在以后才统一对脏页进行刷回磁盘的操做。也就是说,change buffer
避免了每次都须要调入一个数据页进内存中进行修改,形成脏页过多的问题。
第二,也是最重要的,change buffer
的设计避免了在每一次插入过程当中为了寻找数据页而进行的随机IO。而且,在以后对脏页进行刷新的时候,MySQL会尽量的让脏页能够是以顺序IO的方式刷新回磁盘中。
这个过程对于普通索引来讲是提高的很是大的。
简单的来讲,change buffer
的主要目的就是将记录的变动动做缓存下来。因此在一个数据页作merge
以前,change buffer
记录的变动越多(也就是这个页面上要更新的次数越多),收益就越大。
可是对于惟一索引来讲,由于惟一索引的约束是“数据惟一”。因此仍是须要找到这个数据页,判断有无冲突,才会进行插入。这样的话,change buffer
不起做用。
而后咱们来把change buffer
与以前提到的redo log
联系在一块儿。
好比咱们须要插入两条数据,其中一条数据所在的数据页在内存中,另一条数据所在的数据页在磁盘中(还未读入内存),且这两条数据所用到的索引是普通索引(不须要验证是否重复)。
此时,对于在数据页在内存中的插入操做,直接修改内存,对于数据页不在内存中的插入操做,将这个插入操做记录在change buffer
中。随后,将这两次的操做,记录在了redo log
中,而后增长binlog
。当这两个日志文件都写好后,返回,操做结束。
而对于什么时候将内存中的脏页刷回磁盘,是另外的一个操做。
此外,这里的change buffer
也一样能够被持久化,也遵循checkpoint
机制,即change buffer
会标记哪些记录是已经merge
到数据页中,哪些尚未。
在MySQL5.5
之后,除了插入操做,更新操做和删除操做,也支持使用change buffer
。也就是说,对于更新操做和删除操做,也会被change buffer
记录下来,在以后才进行merge
。
首先,谢谢你能看到这里!
此外,也要特别感谢雄哥,在索引这一部分的内容一样给了我特别大的帮助!
关于MySQL索引相关的内容,大概就是这些了。一样的,也在这篇文章中挖了不少坑没有填上。限于篇幅以及文章的连贯性,没有详细介绍。可是会在后面的文章中提到的。
若是在这篇文章中,有什么是我没有解释清楚的,又或者是个人理解出现了错误,还请留言指正,谢谢啦!
PS:若是有其余的问题,也能够在公众号找到做者。而且,全部文章第一时间会在公众号更新,欢迎来找做者玩~