详细介绍了各种高性能的索引使用策略,比如索引排序、索引下推、压缩索引等等,以及常见索引失效的情况。
前面我们已经介绍了各种类型的索引结构及其对应的优缺点:
- BTREE索引的数据结构以及具体实现原理深入解析
- 哈希索引的数据结构以及索引的优缺点
正确的创建和使用索引是实现高性能查询的基础。我们通常会看到一些查询不当的使用索引,或者使用MySQL无法使用已有的索引,下面要讲的高性能的索引策略就是要避免索引失效,并尽可能的发挥这些索引的优势。
上文高性能的索引策略以及常见索引失效的情况(1)中,我们讲解了部分高性能索引策略,下面我们来看看其他的策略。
文章目录
- 1 索引扫描排序
- 2 索引下推
- 3 压缩(前缀压缩)索引
- 4 重复、冗余索引和未使用的索引
- 5 常见索引失效情况
- 6 三星索引
1 索引扫描排序
MySQL可以通过排序操作或者按索引顺序扫描这两种方式来排序。如果EXPLAIN的type列的值为“index”,则说明MySQL使用了索引扫描来做排序。
MySQL可以使用同一个索引既满足排序,又用于查找行,只有当索引列的顺序和ORDER BY字句的要求顺序是一致的,并且所有列的排序方向(倒序或正序时)MySQL才能够使用索引来对结果进行排序。
如果查询需要关联多张表, 则只有当 ORDER BY 子句引用的字段全部为第一个表时,才能使用索引做排序。 ORDER BY 子句和查找型查询的限制是一样的:需要满足索引的最左前缀的要求。否则, MySQL都需要执行排序操作,而无法利用索引排序。
一个例外是,如果前导列为常量的时候,如果WHERE或者JOIN字句中对这些列指定了常量,就可以弥补上面的不足。
一个多列索引(a,b,c),使用如下查询:
SELECT b, c, d FROM tablenames WHERE a = 123 ORDER BY b,c;
因为索引的第一列提供了常量,从而可以使用第二列开始进行排序,将三个列组合在一起,就行形成了索引的最左前缀。下面这个SQL也能使用排序:
…… WHERE a = 123 ORDER BY b DESC;
下面的SQL也能使用排序,因为ORDER BY的两列就是索引的最左前缀:
…… WHERE a > 123 ORDER BY a , b;
下面的SQL不能使用排序,因为ORDER BY使用了两种不同的排序方向:
…… WHERE a = 123 ORDER BY a DESC, b ASC;
下面的SQL不能使用排序,因为ORDER BY使用了一个不在索引中的列:
…… WHERE a = 123 ORDER BY b, d;
下面的SQL不能使用排序,因为WHERE和ORDER BY的列不满足最左前缀:
…… WHERE a = 123 ORDER BY c;
下面的SQL不能使用排序,因为WHERE的第一列上是范围条件,所以MySQL无法使用其他索引列:
…… WHERE a > 123 ORDER BY b,c;
下面的SQL不能使用排序,因为WHERE的第二列上有多个条件,对于排序来说也是范围查询:
…… WHERE a > 123 AND b IN(1,2) ORDER BY c;
无法使用索引排序的时候,将使用文件排序(filessort),但并不一定使用磁盘文件。
2 索引下推
最左前缀可以用于在索引中定位记录,而对于那些不符合最左前缀的查询条件部分,在MySQL 5.6之前只能在回表取到完整的行数据之后再一一比对,而MySQL 5.6 引入的索引下推优化(index condition pushdown,ICP),可以在索引遍历过程中,对索引中包含的字段先做判断(即使该字段没有使用到索引),直接过滤掉不满足条件的记录,减少回表次数,索引下推还支持Where中的“%xx%”模糊查询。注意,该优化不一定会每次都被使用。
假设某个表中存在(name,age)的联合索引,我们需要检索出表中“姓张,而且年龄是10岁的人”。因为最左前缀的原则之一:范围查询的字段之后的索引字段失效,所以此时并没有使用到age这个索引字段。MySQL5.6之前,根据下图中的数据,图中一个箭头表示需要回表一次,name需要回表四次进行查询和筛选。
MySQL5.6及其之后的版本中,虽然age字段还是没有使用到索引,但是由于索引包含该查询字段age,因此InnoDB在(name,age)索引内部就判断了age是否等于10,对于不等于10的记录,直接判断并跳过。如下图,红色的箭头表示不会执行的回表查询,此时只需要回表两次即可,有效减少了IO次数。
3 压缩(前缀压缩)索引
MyISAM使用前缀压缩来减少索引的大小,从而让更多的索引可以放入内存中,这在某些情况下能极大地提高性能。
默认只压缩字符串,但通过参数设置也可以对整数做压缩。
MyISAM压缩每个索引块的方法是:先完全保存索引块中的第一个值,然后将其他值和第一个值进行比较得到相同前缀的字节数和剩余的不同后缀部分,把这部分存储起来即可。例如,索引块中的第一个值是“perform“,第二个值是”performance“,那么第二个值的前缀压缩后存储的是类似”7,ance“这样的形式。MyISAM对行指针也采用类似的前缀压缩方式。
压缩块使用更少的空间,代价是某些操作可能更慢。因为每个值的压缩前缀都依赖前面的值,所以MyISAM查找时无法在索引块使用二分查找而只能从头开始扫描。正序的扫描速度还不错,但是如果是倒序扫描——例如ORDER BY DESC——就不是很好了。所有在块中查找某一行的操作平均都需要扫描半个索引块。
测试表明,对于CPU密集型应用,因为扫描需要随机查找,压缩索引使得MyISAM在索引查找上要慢好几倍。压缩索引的倒序扫描就更慢了。压缩索引需要在CPU内存资源与磁盘之间做权衡。压缩索引可能只需要十分之一大小的磁盘空间,如果是I/O密集型应用,对某些查询带来的好处会比成本多很多。
可以在CREATE TABLE语句中指定PACK_KEYS
参数来控制索引压缩的方式。
4 重复、冗余索引和未使用的索引
MySQL允许在相同列上创建多个索引,并且需要单独维护重复的索引,并且优化器在优化查询的时候也需要逐个的进行考虑,这会影响性能。
重复索引是指在相同的列上按照相同的顺序创建相同类型的索引,应该避免这样创建重复索引,发现后也应该立即删除。
如下建表语句:
CREATE TABLE test (
id BIGINT PRIMARY KEY,
a INT,
UNIQUE ( id ),
INDEX ( id )
);
可能用于希望创建一个主键,并且加上唯一限制,然后加上索引以供查询**。事实上,MySQL的唯一限制和主键限制都是通过索引实现的,因此,上面的写法实际上在相同的列上创建了三个重复的索引,但这是没有必要的。**
冗余索引和重复索引有一些不同,如果创建了多列索引(a,b),再创建索引(a)就是冗余索引,因为这只是前面一个索引的前缀索引,因此(a,b)也可以当作(a)来使用,但是如果再创建(b,a)就不是冗余索引,再创建索引(b)也不是,因为b不是索引(a,b)的最左前缀列,另外,其他不同类型的索引在相同列上创建(如哈希索引和全文索引)不会是B-Tree索引的冗余索引,而无论覆盖的索引列是什么。
冗余索引通常发生再为表添加新索引的时候。例如,有人可能会增加一个新的索引(A,B)而不是扩展以后的索引(A)。还有一种情况是将一个索引扩展为(A,ID),其中ID是主键,对于InnoDB来说主键已经包含在二级索引中了,所以这也是冗余的。
大多数情况下都不需要冗余索引,一种常见的索引优化就是:应该尽量扩展已有的索引而不是创建新索引,但也有时候处于性能方面的考虑需要冗余索引,因为扩展已有的索引会导致其变得太大,从而影响其他使用该索引的查询性能。如:如果在整数列上有一个索引,现在需要额外增加一个很长的varchar列来扩展该索引,那么性可能会急剧下降,特别是有查询把这个索引当作覆盖索引,或者这是MyISAM表并且有很多范围查询的时候(由于MyISAM的前缀压缩)。
怎么找出冗余索引和重复索引呢?
- 可以使用
Shlomi Noach
的common_schema
中的一些视图来定位,common_schema是一系列可以安装到服务器上的常用的存储和试图。 - 可以使用
Percona Toolkit
中的pt_duplicate-key-checker
,该工具通过分析表结构来找出冗余和重复的索引。
在删除冗余索引的时候要非常小心:如果在InnoDB引擎表上有where a=5 order by id 这样的查询,那么索引(a)就会很有用,索引(a,b)实际上是(a,b,id)索引,这个索引对于where a=5 order by id 这样的查询就无法使用索引做排序,而只能使用文件排序了。所以,建议使用percona工具箱中的pt-upgrade工具来仔细检查计划中的索引变更。
除了冗余索引和重复索引,可能还会有一些服务器永远不使用的索引,这样的索引完全是累赘,建议考虑删除,有两个工具可以帮助定位未使用的索引:
- 在
percona server
或者mariadb
中先打开userstat=ON
服务器变量,默认是关闭的,然后让服务器运行一段时间,再通过查询information_schema.index_statistics
就能查到每个索引的使用频率。 - 使用
percona toolkit
中的pt-index-usage
工具,该工具可以读取查询日志,并对日志中的每个查询进行explain操作,然后打印出关羽索引和查询的报告,这个工具不仅可以找出哪些索引是未使用的,还可以了解查询的执行计划,如:在某些情况下有些类似的查询的执行方式不一样,这可以帮助定位到那些偶尔服务器质量差的查询,该工具也可以将结果写入到mysql的表中,方便查询结果。
5 常见索引失效情况
对于索引列使用了函数或者表达式进行计算,一定不会走索引。因为对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能,而实际上Mysql优化器对于不改变有序性的函数,也不会考虑使用索引,没那么智能。
隐式的类型转换。在MySQL中,字符串和数字做比较的话,是将字符串转换成数字,这是一种隐式的类型转换,使用CAST(xx AS signed int)函数。因此,对于数值类型的索引列使用数值字符串进行匹配,可以使用索引,因为MySQL对字符串执行了类型转换,但是如果对于字符串类型的索引列使用数值进行比较,则无法使用索引,因为索引执行了类型转换。
隐式的字符编码转换。比如字符集utf8mb4是utf8的超集,所以当这两个类型的字符串在做比较的时候,MySQL内部的操作是,先把utf8字符串转成utf8mb4字符集,再做比较,使用CONVERT(traideid USING utf8mb4)。因此,对于多表联查的情况,如果两表使用的字符集不同,且被驱动的表的索引列使用utf8编码,则由于索引列执行了编码函数转换,进而无法使用索引。
对于索引列使用LIKE模糊匹配时,如果%在前面,则一定不会走索引。
不符合最左前缀匹配的规则不会走索引(规则在此前已经讲过了)。
某些查询是否一定不会用到索引呢?比如is null、is not null、<>、!=、BETWEEN、not in等等,虽然很对文章都说不会走,但实际上这些都是不能确定的,特别是对于较高版本的mysql,一般来说如果满足这些条件的索引数据占比比较高,那么就不会走索引,如果符合条件的数据很少,则是完全有可能走索引的,并且,如果支持索引覆盖,那么仍然有大概率走索引(用了索引,但是没有用到索引的快速定位能力,而是全索引扫描)。反过来,即使是基于 = 的精确匹配,如果满足条件的索引数据占比比较高,那么仍可能走全表扫描而不是一定会走索引。这些判断都是基于select 条件以及MySQL优化器自己的判断,我们不应该武断的说一定会走或者一定不会走。
6 三星索引
《Relational Database index design and the optimizers》(数据库索引设计与优化)
书中提出了判断索引是否适合某个查询的“三星系统”:
- 将相关记录放到一起则获得一星,即WHERE后面参与查询的列可以组成了单列索引或联合索引,where后面的谓词和索引列匹配的越多,索引片越窄,最终扫描的数据行也是越小。
- 索引中的数据顺序和查找中的排列顺序一致获得二星,如果ORDER BY和索引的顺序一致,则可以利用现成的顺序从而避免了生成临时表并使用文件排序。
- 索引中的列包含了查询中需要的全部列获得三星,避免回表查询,减少了IO操作,它可以保证查询只需访问索引而无需访问表记录。
如果某个索引满足上面的三个条件,则该索引被称为“三星索引”。当然 了三星索引是一个比较理想化的标准,实际操作往往只能满足期望中的一颗或两颗星即可。
参考资料:
- 《 MySQL 技术内幕: InnoDB 存储引擎》
- 《高性能 MySQL》