一起学习Mysql索引二(索引的高性能策略)

通过上一篇的学习,我们理解了Mysql索引背后的数据结构以及MyISAM和InnoDB两个存储引擎中BTree索引的实现原理。理解了索引背后的机制之后,强哥今天就和大家聊一聊Mysql索引的优化。

不过在此之前我们需要先了解一些概念:

  • 三星系统

  • 索引的选择性

  • BTree索引的限制

三星系统

首先我们先来了解一下如何评价一个索引是否适合某个查询的“三星系统”:索引将相关的记录放到一起则获得一星;如果索引中的数据顺序和查找中的排序顺序一致则获得二星;如果索引中的列包含了查询中需要的全部列则获得三星。

根据三星系统,我们能够更好的评价一个索引的好坏。

索引的选择性

索引的选择性是指,不重复的索引值(也称为基数)和数据表的记录总数(rows)的比值,范围从1/rows到1之间。索引的选择性越高则查询效率越高,因为选择性高的索引可以让Mysql在查找时过滤掉更多的行。唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。而假如我们是按某个表的性别字段作为索引列,这样选择性就很低,索引效果就不是很明显。

BTree索引的限制

根据上一篇推文中BTree索引类型查询相关内容我们知道,索引树是有序的,这也造成了一定的限制:

  • 如果不是按照索引的最左列开始查找,则无法使用索引。

  • 不能跳过索引中的列,即不能跳过索引前面的某一列不进行匹配而直接跳到最后一列进行匹配。

  • 如果查询中有某个列的范围查询,则其右边所有列都无法使用索引优化查询。

接下来我们就来聊一些高性能的索引策略:

索引选择性

根据上面提到的索引选择性,我们可以判断出两种不建议创建索引的情况:

  1. 表数据比较少时,例如一两千条甚至只有几百条记录的表,没必要建索引,让查询做全表扫描就好了。至于多少条记录才算多,这个个人有个人的看法,我个人的经验是以5000作为分界线。

  2. 索引选择性比较低时。当选择性很低时,搜索引擎走索引后返回的数据行数和总的数据行数几乎差不多,这样还多了一层走索引的操作,不如直接走全表来的更快。

前缀索引

有时候需要索引的列长度很长,这回让索引变得大且慢。那么就可以使用前缀索引了,也就是用列的前缀代替整个列来建立索引,这样可以大大节约索引空间,从而提高索引效率。比如列长为11(某记录值为:performance),我们截取长为7做前缀索引(索引值为:perform)。

但是这样也会降低索引的选择性,前缀太短,选择性就低,索引就失去了价值。所以需要选择足够长的前缀保证较高的选择性,又不能过长导致索引过大。

我们可以通过下面的语句计算索引列的选择性大小:

SELECT COUNT(DISTINCT 索引字段名)/COUNT(*) FROM 表;//单列索引
SELECT COUNT(DISTINCT (LEFT(索引字段名,前缀长度)))/COUNT(*) FROM 表;//单列前缀索引
SELECT COUNT(DISTINCT (CONCAT(索引字段名1,索引字段名2)))/COUNT(*) FROM 表;//多列索引
SELECT COUNT(DISTINCT (CONCAT(索引字段名1,LEFT(索引字段名2,前缀长度))))/COUNT(*) FROM 表;//多列前缀索引

当选择性足够大且索引长度不是很大时,我们便可以建立该前缀索引。

选择合适的索引列顺序

在一个多列BTree索引中,索引列的顺序意味着索引首先按照最左列进行排序,其次是第二列,等等。所以,索引可以按照升序或者降序进行扫描,以满足精确符合列顺序的ORDER BY、GROUP BY和DISTINCT等子句的查询需求。所以多列索引的列顺序至关重要。

对于如何选择索引的列顺序呢?根据上面选择性的表述我们是不是可以得出如下结论:将选择性最高的列放到索引最前列。

当不需要考虑排序和分组时,将选择性最高的列放到索引最前面通常是很好的。这时候索引的作用只是用于优化WHERE条件的查找。然而,性能不只是依赖于所有索引列的选择性,也和查询条件的具体值有关,也就是和值的分布有关。

当使用前缀索引的时候,在某些条件值的技术比正常值高的时候,问题就来了。例如,在某些应用程序中,有一个特殊的管理员账号,和普通账号不同,系统中的所有其他用户都是这个用户的好友。这个账号巨大的好友列表很容易导致网站出现服务器性能问题。在这种情况下,即使使用了所有,但是获取该用户好友列表时,还是获取了表中的几乎所有记录--也就是说索引基本上没什么用。

这种情况的解决办法是修改应用程序代码,区分这类特殊用户,禁止针对这类用户执行这样的查询操作。而不是使用索引。

在Innodb中推荐使用自增主键

在上一篇推文中,我们知道了Mysql中的Innodb引擎使用最多的是BTree聚簇索引,数据记录本身被存于主索引的叶子节点上。这就要求同一个叶子节点内(大小为一个内存页或磁盘页)的各条数据记录按主键顺序存放,因此每当有一条新的记录插入时,MySQL会根据其主键将其插入适当的节点和位置,如果页面达到装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)。

如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。如下图所示:

这样就会形成一个紧凑的索引结构,近似顺序填满。由于每次插入时也不需要移动已有数据,因此效率很高,也不会增加很多开销在维护索引上。

如果使用非自增主键(如果身份证号或学号等),由于每次插入主键的值近似于随机,因此每次新纪录都要被插到现有索引页得中间某个位置:

此时MySQL不得不为了将新记录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,此时又要从磁盘上读回来,这增加了很多开销,同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE语句来重建表并优化填充页面。因此,只要可以,请尽量在InnoDB上采用自增字段做主键。

当然上诉没有考虑到使用自增id在分布式数据库系统中id重复问题,不过我们也可以通过一个专门生成自增id的库表或redis来解决该问题,当然也可以使用snowflake算法。总之,了解了BTree聚簇索引后,我们就可以使用学到的知识去创建相对高效合理的主键了。

延迟关联实现部分覆盖索引

同样上一篇推文中也提过了“覆盖索引“的概念,当某个索引的索引列覆盖了select的所有字段时,会使用到覆盖索引。覆盖索引能够只通过索引就获取到所需要的数据而不需要在回表一条条的查询,同时由于索引是有顺序的,这样对于I/O密集型的范围查询效率也很高。

但是,往往我们很少能够遇到覆盖索引的情况,一般情况都是select的列会多于索引列,这样就无法使用到覆盖索引。那么有什么办法可以让我们使用到覆盖索引呢?

这时候我们便可以使用延迟关联,即在查询的第一几段Mysql可以使用覆盖索引,在FROM子句的子查询中找到匹配的数据Id(或其他唯一属性字段),然后根据这些id值在外层查询匹配获取需要的所有列值。这样虽然无法使用索引覆盖这个查询,但总算比完全无法利用索引覆盖要好一些。

不过这里强哥也要提一句,Mysql5.6版本在存储引擎API上做了一个重要的改进,其被称为“索引条件推送(index condition pushdown)”。这个特性将大大改善现在的查询方式,下一篇推文中强哥将向大家介绍,敬请期待。

使用索引扫描来做排序

扫描索引本身是很快的,因为只需要从一条索引记录移动到紧接着的下一条记录。但如果索引不能覆盖查询所需的全部列,那就不得不没扫描一条索引记录就都回表查询一次对应的行。这基本上都是随机I/O,因此按索引顺序读取数据的速度通常要比顺序地全表扫描慢,尤其是I/O密集型的工作负载时。

只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的顺序方向(倒叙或正序)都一样时,Mysql才能使用索引对结果做排序。如果索引关联多张表,则只有当ORDER BY子句引用的字段全部为第一个表时,才能使用索引做排序。ORDER BY子句需要满足索引的最左前缀的要求,否则,Mysql都需要执行排序操作,而无法使用索引排序。

有一种情况下ORDER BY子句可以不满足索引的最左前缀的需求,就是前导列为常量的时候。如果WHERE子句或者JOIN子句中对这些列指定了常量,就可以弥补索引的不足。

如果需要按不同的方向做排序,一个技巧是存储该列值时,如果是字符串形式,则存储值的反转串;如果是数值类型的,则存储值的反转数。这样在排序的时候就可以获取到最终想要的该列与其他列不同方向的排序了。

总结

根据上面的学习我们可以看到,了解Mysql索引的数据结构,对于索引创建及优化是多么的重要。了解的底层的一些细节,我们就可以对自己的代码操作更有底气,而不是盲目猜测试探或者使用他人的经验法则来处理遇到的问题。

不羡招式,勤修内功,愿你早日成神。

关注公众号获取更多内容,有问题也可在公众号提问哦:

强哥叨逼叨

叨逼叨编程、互联网的见解和新鲜事

发布了54 篇原创文章 · 获赞 69 · 访问量 25万+

猜你喜欢

转载自blog.csdn.net/seanxwq/article/details/93663081
今日推荐