面向面试编程:MySQL的索引(三)

165 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第9天,点击查看活动详情

面试官:联合索引你有了解过吗?

联合索引

我们设计系统的时候一般都是设计联合索引,很少用单个字段做索引,我们还是要尽可能的让索引数量少一些,避免磁盘占用太多,增删改性能太差。

等值匹配规则

SQL语句里,where条件里的几个字段都是基于等值来查询,都是用的等于号!而且where条件里的几个字段的名称和顺序也跟你的联合索引一模一样!此时就是等值匹配规则,SQL语句是百分百可以用联合索引来查询的。

查询原理

查询的过程也很简单,首先到索引页里去找,索引页里有多个数据页的最小值记录,此时直接在索引页里基于二分查找法来找就可以了,直接可以定位到他所在的数据页。

然后就直接找到索引指向的那个数据页就可以了,在数据页内部本身也是一个单向链表,也是直接就做二分查找就可以了。然后就根据主键id到聚簇索引里按照一样的思路,从索引根节点开始二分查找迅速定位下个层级的页,再不停的找,很快就可以找到那条数据,然后从里面提取所有字段,就可以了。

假设你的SQL语句的where条件里用的几个字段的名称和顺序,都跟你的索引里的字段一样,同时你还是用等号在做等值匹配,那么直接就会按照上述过程来找。对于联合索引而言,就是依次按照各个字段来进行二分查找,先定位到第一个字段对应的值在哪个页里,然后如果第一个字段有多条数据值都一样,就根据第二个字段来找,以此类推,一定可以定位到某条或者某几条数据!

面试官:刚才说到了等值匹配规则,你还了解哪些索引使用规则呢?

索引使用规则

等值匹配规则

就是你where语句中的几个字段名称和联合索引的字段完全一样,而且都是基于等号的等值匹配,那一定会用上我们的索引,这个是没有问题的,即使你where语句里写的字段的顺序和联合索引里的字段顺序不一致,也没关系,MySQL会自动优化为按联合索引的字段顺序去找。

最左侧列匹配

假设我们联合索引是KEY(class_name, student_name, subject_name),那么不一定必须要在where语句里根据三个字段来查,其实只要根据最左侧的部分字段来查,也是可以的。

最左前缀匹配原则

即如果你要用like语法来查,比如select * from student_score where class_name like '1%',查找所有1打头的班级的分数,那么也是可以用到索引的。因为你的联合索引的B+树里,都是按照class_name排序的,所以你要是给出class_name的确定的最左前缀就是1,然后后面的给一个模糊匹配符号,那也是可以基于索引来查找的,这是没问题的。

范围查找规则

这个意思就是说,我们可以用select * from student_score where class_name>'1班' and class_name<'5班'这样的语句来范围查找某几个班级的分数。这个时候也是会用到索引的,因为我们的索引的最下层的数据页都是按顺序组成双向链表的,所以完全可以先找到'1班'对应的数据页,再找到'5班'对应的数据页,两个数据页中间的那些数据页,就全都是在你范围内的数据了!

等值匹配+范围匹配的规则

如果你要是用select * from student_score where class_name='1班' and student_name>'' and subject_name<'',那么此时你首先可以用class_name在索引里精准定位到一波数据,接着这波数据里的student_name都是按照顺序排列的,所以student_name>''也会基于索引来查找,但是接下来的subject_name<''是不能用索引的。

所以综上所述,一般我们如果写SQL语句,都是用联合索引的最左侧的多个字段来进行等值匹配+范围搜索,或者是基于最左侧的部分字段来进行最左前缀模糊匹配,或者基于最左侧字段来进行范围搜索,这就要写符合规则的SQL语句,才能用上我们建立好的联合索引!

面试官:我们在SQL里进行排序的话可以用到索引吗?

SQL排序如何使用索引

假设有一个select * from table where xxx=xxx order by xxx这样的一个SQL语句,似乎应该是基于where语句通过索引快速筛选出来一波数据,接着放到内存里,或者放在一个临时磁盘文件里,然后通过排序算法按照某个字段走一个排序,最后把排序好的数据返回。但是这么搞通常速度有点慢,尤其是万一要排序的数据量比较大的话,还不能用内存来排序,如果基于磁盘文件来排序,那在MySQL里有一个术语,叫做filesort,这速度就比较慢了。

尽量是最好别这么搞,尤其是类似于select * from table order by xx1,xx2,xx3 limit 100这样的SQL语句,按照多个字段进行排序然后返回排名前100条数据,类似的语句其实常常见于分页SQL语句里,可能需要对表里的数据进行一定的排序,然后走一个limit拿出来指定部分的数据。要是纯粹把一坨数据放到一个临时磁盘文件里,然后直接硬上各种排序算法在磁盘文件里搞一通排序,接着按照指定的要求走limit语句拿到指定分页的数据,这简直会让SQL的速度慢到家了!

这种情况下,假设我们建立了一个INDEX(xx1,xx2,xx3)这样的一个联合索引,这个时候默认情况下在索引树里本身就是依次按照xx1,xx2,xx3三个字段的值去排序的,那么此时你再运行select * from table order by xx1,xx2,xx3 limit 100这样的SQL语句,还需要在什么临时磁盘文件里排序吗?显然是不用了啊!因为他要求也不过就是按照xx1,xx2,xx3三个字段来进行排序罢了,在联合索引的索引树里都排序好了,直接就按照索引树里的顺序,把xx1,xx2,xx3三个字段按照从小到大的值获取前面100条就可以了。然后拿到100条数据的主键再去聚簇索引里回表查询剩余所有的字段。

SQL语句里,应该尽量最好是按照联合索引的字段顺序去进行order by排序,这样就可以直接利用联合索引树里的数据有序性,到索引树里直接按照字段值的顺序去获取你需要的数据了。但是这里有一些限定规则,因为联合索引里的字段值在索引树里都是从小到大依次排列的 ,所以你在order by里要不然就是每个字段后面什么都不加,直接就是order by xx1,xx2,xx3,要不然就都加DESC降序排列,就是order by xx1 DESC,xx2 DESC,xx3 DESC。

如果都是升序排列,直接就从索引树里最小的开始读取一定条数就可以了,要是都是降序排列,就是从索引树里最大的数据开始读取一定的条数就可以了,但是不能order by语句里有的字段升序有的字段降序,那是不能用索引的。另外,要是order by语句里有的字段不在联合索引里,或者是对order by语句里的字段用了复杂的函数,这些也不能使用索引去进行排序了。

面试官:在SQL里进行分组可以使用索引吗?

SQL分组如何使用索引

在SQL语句里假设要是用到了group by分组语句的话是否可以用上索引,有时候我们会想要做一个group by把数据分组接着用count sum之类的聚合函数做一个聚合统计。那假设你要是走一个类似select count(1) from table group by xx的SQL语句,似乎看起来必须把你所有的数据放到一个临时磁盘文件里还有加上部分内存,去搞一个分组,按照指定字段的值分成一组一组的,接着对每一组都执行一个聚合函数,这个性能也是极差的,因为毕竟涉及大量的磁盘交互。

因为在我们的索引树里默认都是按照指定的一些字段都排序好的,其实字段值相同的数据都是在一起的,假设要是走索引去执行分组后再聚合,那性能一定是比临时磁盘文件去执行好多了。所以通常而言,对于group by后的字段,最好也是按照联合索引里的最左侧的字段开始,按顺序排列开来,这样的话,其实就可以完美的运用上索引来直接提取一组一组的数据,然后针对每一组的数据执行聚合函数就可以了。

其实这个group by和order by用上索引的原理和条件都是差不多的,本质都是在group by和order by之后的字段顺序和联合索引中的从最左侧开始的字段顺序一致,然后就可以充分利用索引树里已经完成排序的特性,快速的根据排序好的数据执行后续操作了。这样就不再需要针对杂乱无章的数据利用临时磁盘文件加上部分内存数据结构进行耗时耗力的现场排序和分组,那真是速度极慢,性能极差的。

在考虑好之后,就可以为表设计两三个常用的索引,覆盖常见的where筛选、order by排序和group by分组的需求,保证常见的SQL语句都可以用上索引,这样你真正系统跑起来,起码是不会有太大的查询性能问题了。毕竟只要所有的查询语句都可以利用索引来执行,那么速度和性能通常都不会太慢。如果查询还是有问题,那就要深度理解查询的执行计划和执行原理了,然后基于执行计划来进行深度SQL调优。

对于更新语句而言,其实最核心的就是三大问题,一个是你索引别太多,索引太多了,更新的时候维护很多索引树肯定是不行的;一个是可能会涉及到一些锁等待和死锁的问题;一个就是可能会涉及到MySQL连接池、写redo log文件之类的问题。