本文主要通过创建一个abcd 四字段的联合索引来分析各个组合查询场景下索引的使用情况,同时之前在做xqueue 项目时,在压测中,发现因为order by后的字段没有使用索引,使得explain 中 ref 包含using filesort,性能一直压不上去,进而分析为什么会出现using filesort以及如何避免using filesort问题。
准备
1.创建test表(测试表)。
drop table if exists test;
create table test(
id int primary key auto_increment,
c1 varchar(10),
c2 varchar(10),
c3 varchar(10),
c4 varchar(10),
c5 varchar(10)
) ENGINE=INNODB default CHARSET=utf8;
insert into test(c1,c2,c3,c4,c5) values('a1','a2','a3','a4','a5');
insert into test(c1,c2,c3,c4,c5) values('b1','b2','b3','b4','b5');
insert into test(c1,c2,c3,c4,c5) values('c1','c2','c3','c4','c5');
insert into test(c1,c2,c3,c4,c5) values('d1','d2','d3','d4','d5');
insert into test(c1,c2,c3,c4,c5) values('e1','e2','e3','e4','e5');
2.创建索引。
1.根据以下Case分析索引的使用情况
Case 1:
分析:
①创建复合索引的顺序为c1,c2,c3,c4。
②上述四组explain执行的结果都一样:type=ref,key_len=132,ref=const,const,const,const。
结论:在执行常量等值查询时,改变索引列的顺序并不会更改explain的执行结果,因为mysql底层优化器会进行优化,但是推荐按照索引顺序列编写sql语句。
Case 2:
分析:
当出现范围的时候,type=range,key_len=99,比不用范围key_len=66增加了,说明使用上了索引,但对比Case1中执行结果,说明c4上索引失效。
结论:范围右边索引列失效,但是范围当前位置(c3)的索引是有效的,从key_len=99可证明。
Case 2.1:
分析:
与上面explain执行结果对比,key_len=132说明索引用到了4个,因为对此sql语句mysql底层优化器会进行优化:范围右边索引列失效(c4右边已经没有索引列了),注意索引的顺序(c1,c2,c3,c4),所以c4右边不会出现失效的索引列,因此4个索引全部用上。
结论:范围右边索引列失效,是有顺序的:c1,c2,c3,c4,如果c3有范围,则c4失效;如果c4有范围,则没有失效的索引列,从而会使用全部索引。
Case 2.2:
分析:
如果在c1处使用范围,则type=ALL,key=Null,索引失效,全表扫描,这里违背了最佳左前缀法则,带头大哥已死,因为c1主要用于范围,而不是查询。
解决方式使用覆盖索引。
结论:在最佳左前缀法则中,如果最左前列(带头大哥)的索引失效,则后面的索引都失效。
Case 3:
分析:
利用最佳左前缀法则:中间兄弟不能断,因此用到了c1和c2索引(查找),从key_len=66,ref=const,const,c3索引列用在排序过程中。
Case 3.1:
分析:
从explain的执行结果来看:key_len=66,ref=const,const,从而查找只用到c1和c2索引,c3索引用于排序。
Case 3.2:
分析:
从explain的执行结果来看:key_len=66,ref=const,const,查询使用了c1和c2索引,由于用了c4进行排序,跳过了c3,出现了Using filesort。
Case 4:
分析:
查找只用到索引c1,c2和c3用于排序,无Using filesort。
Case 4.1:
分析:
和Case 4中explain的执行结果一样,但是出现了Using filesort,因为索引的创建顺序为c1,c2,c3,c4,但是排序的时候c2和c3颠倒位置了。
Case 4.2:
分析:
在查询时增加了c5,但是explain的执行结果一样,因为c5并未创建索引。
Case 4.3:
分析:
与Case 4.1对比,在Extra中并未出现Using filesort,因为c2为常量,在排序中被优化,所以索引未颠倒,不会出现Using filesort。
Case 5:
分析:
只用到c1上的索引,因为c4中间间断了,根据最佳左前缀法则,所以key_len=33,ref=const,表示只用到一个索引。
Case 5.1:
分析:
对比Case 5,在group by时交换了c2和c3的位置,结果出现Using temporary和Using filesort,极度恶劣。原因:c3和c2与索引创建顺序相反。
总结:
通过以上Case的分析,进行如下总结:
①最佳左前缀法则。
在等值查询时,更改索引列顺序,并不会影响explain的执行结果,因为mysql底层会进行优化。
在使用order by时,注意索引顺序、常量,以及可能会导致Using filesort的情况。
②group by容易产生Using temporary。
③通俗理解口诀:
全值匹配我最爱,最左前缀要遵守;
带头大哥不能死,中间兄弟不能断;
索引列上少计算,范围之后全失效;
LIKE百分写最右,覆盖索引不写星;
不等空值还有or,索引失效要少用。
using filesort 分析
我们先建一个user表,其中有自增主键、user_id 也建立索引,create_date暂时不建索引,省略其他字段。
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '员工id',
`create_date` datetime NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '创建日期',
省略其他字段
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
当分页查询的时候,打印执行计划,Extra 一栏出现了 Using filesort。
explain SELECT * FROM user ORDER BY create_date DESC limit 20.40;
+----+-------------+-------+------+---------------+------+---------+------+-------+----------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+-------+----------------+
| 1 | SIMPLE | user | ALL | NULL | NULL | NULL | NULL | 22686 | Using filesort |
+----+-------------+-------+------+---------------+------+---------+------+-------+----------------+
Using filesort 是什么意思?
官方的定义是,MySQL must do an extra pass to find out how to retrieve the rows in sorted order. The sort is done by going through all rows according to the join type and storing the sort key and pointer to the row for all rows that match the WHERE clause . The keys then are sorted and the rows are retrieved in sorted order。
MySQL需要额外的一次传递,以找出如何按排序顺序检索行。通过根据联接类型浏览所有行并为所有匹配WHERE子句的行保存排序关键字和行的指针来完成排序。然后关键字被排序,并按排序顺序检索行。标红,重点。
filesort 有两种排序方式
- 对需要排序的记录生成 <sort_key,rowid> 的元数据进行排序,该元数据仅包含排序字段和rowid。排序完成后只有按字段排序的rowid,因此还需要通过rowid进行回表操作获取所需要的列的值,可能会导致大量的随机IO读消耗;
- 对需要排序的记录生成 <sort_key,additional_fields> 的元数据,该元数据包含排序字段和需要返回的所有列。排序完后不需要回表,但是元数据要比第一种方法长得多,需要更多的空间用于排序。
优化方法
filesort 使用的算法是QuickSort,即对需要排序的记录生成元数据进行分块排序,然后再使用mergesort方法合并块。其中filesort可以使用的内存空间大小为参数 sort_buffer_size 的值,默认为2M。当排序记录太多 sort_buffer_size 不够用时,mysql会使用临时文件来存放各个分块,然后各个分块排序后再多次合并分块最终全局完成排序。可以增大 **sort_buffer_size **来解决 filesort 慢问题,也就是上面的第二种排序。
当 排序元组中的extra列的总大小不超过 max_length_for_sort_data 系统变量值的时候,我们如何优化 Using filesort 中的 回表操作 呢?
文件排序优化不仅用于记录排序关键字和行的位置,并且还记录查询需要的列。这样可以避免两次读取行。
我们都知道,Mysql Innodb 下使用的是聚集索引。PRIMARY KEY 的叶子节点存储的是数据,其他索引的叶子节点存储的是PRIMARY KEY.
当我们使用非PRIMARY KEY 查询的时候,查询1会进行回表操作,也就是额外的一次查询,去查询表中的其他数据,而查询2会直接返回id和user_id。
查询1:select * from user where user_id=1;
查询2:select user_id,id from user where user_id=1;
好,下面我们通过例子来理解一下Using filesort 的形成
我们先来看第一条查询,根据create_date 排序,由于create_date 没有建索引,
explain SELECT create_date,id FROM user ORDER BY create_date DESC;
+----+-------------+-------+------+---------------+------+---------+------+-------+----------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+-------+----------------+
| 1 | SIMPLE | user | ALL | NULL | NULL | NULL | NULL | 22686 | Using filesort |
+----+-------------+-------+------+---------------+------+---------+------+-------+----------------+
当使用没有索引字段的时候,会出现 Using filesort。那我们建立 create_date 索引之后呢,看下结果。
explain SELECT create_date,id FROM user ORDER BY create_date DESC;
+----+-------------+-------+------+---------------+------+---------+------+-------+----------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+-------+----------------+
| 1 | SIMPLE | user | ALL | NULL | NULL | NULL | NULL | 22686 | Using index |
+----+-------------+-------+------+---------------+------+---------+------+-------+----------------+
如果我想把 user_id 也查询出来呢,看下结果。聚集索引,想查询user_id 还是要进行回表操作的。
explain SELECT create_date,user_id FROM user ORDER BY create_date DESC;
+----+-------------+-------+------+---------------+------+---------+------+-------+----------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+-------+----------------+
| 1 | SIMPLE | user | ALL | NULL | NULL | NULL | NULL | 22686 | Using filesort |
+----+-------------+-------+------+---------------+------+---------+------+-------+----------------+
回到开篇,我们如何优化这个查询呢。
首先是尽量不要让sql使用using filesort。
select * from (select id from user order by create_date DESC limit 20.40) a left join user b on a.id=b.id;
利用mysql聚集索引的性质,分页查询id,避免了Using filesort,这个查询是很快的。而在分页的数据量上,再去查询所有数据,性能就很高了。
如果必须使用using filesort,无法避免排序操作时,很显然应该尽可能让 MySQL 选择使用第二种单路算法来进行排序。这样可以减少大量的随机IO操作,很大幅度地提高排序工作的效率。
- 加大 max_length_for_sort_data 参数的设置
当所有返回字段的最大长度小于这个参数值时,MySQL 就会选择改进后的单路排序,反之,则选择老式的双路排序。所以,如果有充足的内存让MySQL 存放须要返回的非排序字段,就可以加大这个参数的值来让 MySQL 选择使用改进版的排序算法。 - 去掉不必要的返回字段
当内存不是很充裕时,不能简单地通过强行加大上面的参数来强迫 MySQL 去使用改进版的排序算法,否则可能会造成 MySQL 不得不将数据分成很多段,然后进行排序,这样可能会得不偿失。此时就须要去掉不必要的返回字段,让返回结果长度适应 max_length_for_sort_data 参数的限制。 - 增大 sort_buffer_size 参数
设置增大 sort_buffer_size 并不是为了让 MySQL选择改进版的排序算法,而是为了让MySQL尽量减少在排序过程中对需要排序的数据进行分段,因为分段会造成 MySQL 使用临时表来进行交换排序。
参考链接:
mysql索引常见面试题