1、MySQL的基本架构
1)MySQL的基础架构图
2)查询数据库的引擎
3)指定数据库对象的存储引擎
2、SQL优化
1)为什么需要进行SQL优化?
2)mysql的编写过程和解析过程
3)SQL优化—主要就是优化索引
4)索引的弊端
5)索引的优势
6)B+树图示说明
3、索引的分类与创建
1)索引分类
2)创建索引的两种方式
3)索引删除和索引查询
4、SQL性能问题的探索
1)查看执行计划
2)“执行计划”中需要知道的几个“关键字”
5、explain执行计划常用关键字详解
1)id关键字的使用说明
2)select_type关键字的使用说明:查询类型
3)type关键字的使用说明:索引类型
4)possible_keys和key
5)key_len
6)ref
7)rows
8)extra
6、优化示例
1)引入案例
2)单表优化
3)两表优化
4)三表优化
7、避免索引失效的一些原则
8、一些其他的优化方法
1)exists和in的优化
2)order by优化
左边的client可以看成是客户端,客户端有很多,像我们经常你使用的CMD黑窗口,像我们经常用于学习的WorkBench,像企业经常使用的Navicat工具,它们都是一个客户端。右边的这一大堆都可以看成是Server(MySQL的服务端),我们将Server在细分为sql层和存储引擎层。
首先,需要建立客户端与服务器之间的连接。这里通过一个【连接器】,我们建立的客户端与服务器之间的连接,此时,你在客户端写的SQL语句,就可以发送到MySQL的服务了。(接下来要做的事儿肯定不是立即去分析呀!查询底层的数据呀!这样的话效率就太低下了。)
接着,这个SQL语句将会被交到这个【查询缓存】中,如果可以查到,就直接响应回来给你;如果在查询缓存中没有查到,就需要接着往下走。
然后,这个SQL语句将会被交到这个【分析器】中,这个分析器用于词法分析、语法分析,检查你写的SQL语句有没有单词拼写错误,语法书写错误。如果都没有错误,就需要接着往下走。
再接着,这个SQL语句将会被交到这个【优化器】中,优化器如果觉得你的SQL写的太差了,它会帮你写一个性能高一些的等价SQL,去执行。如果优化器觉得你的SQL写的还行,就不会动你的SQL语句。这个优化器与我们下面需要讲解的“索引”有着千丝万缕的关系。
再然后,优化器将最终确定好的SQL方案,交给了【执行器】,执行器通过执行引擎调用“存储引擎”。
最后,“存储引擎”最终调用【文件系统】,从底层去查询出数据。
当查询出数据以后,会返回给执行器。执行器一方面将结果写到查询缓存里面,当你下次再次查询的时候,就可以直接从查询缓存中获取到数据了。另一方面,直接将结果响应回客户端。
create table tb(
id int(4) auto_increment,
name varchar(5),
dept varchar(5),
primary key(id)
) engine=myISAM auto_increment=1 default charset=utf8;
select dinstinct ..from ..join ..on ..where ..group by ..having ..order by ..limit ..
from .. on.. join ..where ..group by ..having ..select dinstinct ..order by ..limit ..
提供一个网站,详细说明了mysql解析过程:https://www.cnblogs.com/annsshadow/p/5037667.html
索引是怎么查找数据的呢?两个字【指向】,上图中我们给age列指定了一个索引,即类似于右侧的这种树形结构。mysql表中的每一行记录都有一个硬件地址,例如索引中的age=50,指向的就是源表中该行的标识符(“硬件地址”)。也就是说,树形索引建立了与源表中每行记录硬件地址的映射关系,当你指定了某个索引,这种映射关系也就建成了,这就是为什么我们可以通过索引快速定位源表中记录的原因。
以【select * from student where age=33】查询语句为例。当我们不加索引的时候,会从上到下扫描源表,当扫描到第5行的时候,找到了我们想要找到了元素,一共是查询了5次。当添加了索引以后,就直接在树形结构中进行查找,33比50小,就从左侧查询到了23,33大于23,就又查询到了右侧,这下找到了33,整个索引结束,一共进行了3次查找。是不是很方便,假如我们此时需要查找age=62,你再想想“添加索引”前后,查找次数的变化情况。
关于B+树的说明:
首先,Btree一般指的都是【B+树】,数据全部存放在叶子节点中。对于上图来说,最下面的第3层,属于叶子节点,真实数据部份都是存放在叶子节点当中的。那么对于第1、2层中的数据又是干嘛的呢?答:用于分割指针块儿的,比如说小于26的找P1,介于26-30之间的找P2,大于30的找P3。
其次,三层【B+树】可以存放上百万条数据。这么多数据怎么放的呢?增加“节点数”。图中我们只有三个节点。
最后,【B+树】中查询任意数据的次数,都是n次,n表示的是【B+树】的高度。
create table tb(
id int(4) auto_increment,
name varchar(5),
dept varchar(5),
primary key(id)
) engine=myISAM auto_increment=1 default charset=utf8;
Ⅰ 创建单值索引
create index dept_index on tb(dept);
Ⅱ 创建唯一索引:这里我们假定name字段中的值都是唯一的
create unique index name_index on tb(name);
Ⅲ 创建复合索引
create index dept_name_index on tb(dept,name);
Ⅰ 创建单值索引
alter table tb add index dept_index(dept);
Ⅱ 创建唯一索引:这里我们假定name字段中的值都是唯一的
alter table tb add unique index name_index(name);
Ⅲ 创建复合索引
alter table tb add index dept_name_index(dept,name);
drop index name_index on tb;
show index from tb;
建表语句和插入数据:
# 建表语句
create table course
(
cid int(3),
cname varchar(20),
tid int(3)
);
create table teacher
(
tid int(3),
tname varchar(20),
tcid int(3)
);
create table teacherCard
(
tcid int(3),
tcdesc varchar(200)
);
# 插入数据
insert into course values(1,'java',1);
insert into course values(2,'html',1);
insert into course values(3,'sql',2);
insert into course values(4,'web',3);
insert into teacher values(1,'tz',1);
insert into teacher values(2,'tw',2);
insert into teacher values(3,'tl',3);
insert into teacherCard values(1,'tzdesc') ;
insert into teacherCard values(2,'twdesc') ;
insert into teacherCard values(3,'tldesc') ;
# 查看执行计划
explain select t.*
from teacher t,course c,teacherCard tc
where t.tid = c.tid and t.tcid = tc.tcid
and (c.cid = 2 or tc.tcid = 3);
insert into teacher values(4,'ta',4);
insert into teacher values(5,'tb',5);
insert into teacher values(6,'tc',6);
再次查看执行计划。
# 查看执行计划
explain select t.*
from teacher t,course c,teacherCard tc
where t.tid = c.tid and t.tcid = tc.tcid
and (c.cid = 2 or tc.tcid = 3);
结果如下:
表的执行顺序 ,因表数量改变而改变的原因:笛卡尔积。
a b c
2 3 4
最终:2 * 3 * 4 = 6 * 4 = 24
c b a
4 3 2
最终:4 * 3 * 2 = 12 * 2 = 24
分析:最终执行的条数,虽然是一致的。但是中间过程,有一张临时表是6,一张临时表是12,很明显6 < 12,对于内存来说,数据量越小越好,因此优化器肯定会选择第一种执行顺序。
结论:id值相同,从上往下顺序执行。表的执行顺序因表数量的改变而改变。
# 查看执行计划
explain select tc.tcdesc from teacherCard tc
where tc.tcid =
(
select t.tcid from teacher t
where t.tid =
(select c.tid from course c where c.cname = 'sql')
);
结果如下:
结论:id值不同,id值越大越优先查询。这是由于在进行嵌套子查询时,先查内层,再查外层。
# 查看执行计划
explain select t.tname ,tc.tcdesc from teacher t,teacherCard tc
where t.tcid= tc.tcid
and t.tid = (select c.tid from course c where cname = 'sql') ;
结果如下:
结论:id值有相同,又有不同。id值越大越优先;id值相同,从上往下顺序执行。
explain select * from teacher;
explain select cr.cname
from ( select * from course where tid = 1 union select * from course where tid = 2 ) cr ;
explain select tid from test01 where tid =1 ;
结果如下:
删除以前的主键索引后,此时我们添加一个其他的普通索引:
create index test01_index on test01(tid) ;
# 再次查看执行计划
explain select tid from test01 where tid =1 ;
delete from teacher where tcid >= 4;
alter table teacherCard add constraint pk_tcid primary key(tcid);
alter table teacher add constraint uk_tcid unique index(tcid) ;
explain select t.tcid from teacher t,teacherCard tc where t.tcid = tc.tcid ;
结果如下:
总结:以上SQL,用到的索引是t.tcid,即teacher表中的tcid字段;如果teacher表的数据个数和连接查询的数据个数一致(都是3条数据),则有可能满足eq_ref级别;否则无法满足。条件很苛刻,很难达到。
# 添加索引
alter table teacher add index index_name (tname) ;
# 查看执行计划
explain select * from teacher where tname = 'tz';
# 添加索引
alter table teacher add index tid_index (tid) ;
# 查看执行计划:以下写了一种等价SQL写法,查看执行计划
explain select t.* from teacher t where t.tid in (1,2) ;
explain select t.* from teacher t where t.tid <3 ;
注意:cid是索引字段,因此查询索引字段,只需要扫描索引表即可。但是tid不是索引字段,查询非索引字段,需要暴力扫描整个源表,会消耗更多的资源。
# 先给course表的cname字段,添加一个索引
create index cname_index on course(cname);
# 查看执行计划
explain select t.tname ,tc.tcdesc from teacher t,teacherCard tc
where t.tcid= tc.tcid
and t.tid = (select c.tid from course c where cname = 'sql') ;
结果如下:
有一点需要注意的是:如果possible_key/key是NULL,则说明没用索引。
# 创建表
create table test_kl
(
name char(20) not null default ''
);
# 添加索引
alter table test_kl add index index_name(name) ;
# 查看执行计划
explain select * from test_kl where name ='' ;
结果如下:
结果分析:因为我没有设置服务端的字符集,因此默认的字符集使用的是latin1,对于latin1一个字符代表一个字节,因此这列的key_len的长度是20,表示使用了name这个索引。
# 新增一个字段name1,name1可以为null
alter table test_kl add column name1 char(20) ;
# 给name1字段,设置为索引字段
alter table test_kl add index index_name1(name1) ;
# 查看执行计划
explain select * from test_kl where name1 ='' ;
结果如下:
结果分析:如果索引字段可以为null,则mysql底层会使用1个字节用于标识。
# 删除原来的索引name和name1
drop index index_name on test_kl ;
drop index index_name1 on test_kl ;
# 增加一个复合索引
create index name_name1_index on test_kl(name,name1);
# 查看执行计划
explain select * from test_kl where name1 = '' ; --121
explain select * from test_kl where name = '' ; --60
结果如下:
结果分析:对于下面这个执行计划,可以看到我们只使用了复合索引的第一个索引字段name,因此key_len是20,这个很清楚。再看上面这个执行计划,我们虽然仅仅在where后面使用了复合索引字段中的name1字段,但是你要使用复合索引的第2个索引字段,会默认使用了复合索引的第1个索引字段name,由于name1可以是null,因此key_len = 20 + 20 + 1 = 41呀!
# 新增一个字段name2,name2可以为null
alter table test_kl add column name2 varchar(20) ;
# 给name2字段,设置为索引字段
alter table test_kl add index name2_index(name2) ;
# 查看执行计划
explain select * from test_kl where name2 = '' ;
结果如下:
结果分析:key_len = 20 + 1 + 2,这个20 + 1我们知道,这个2又代表什么呢?原来varchar属于可变长度,在mysql底层中,用2个字节标识可变长度。
# 给course表的tid字段,添加一个索引
create index tid_index on course(tid);
# 查看执行计划
explain select * from course c,teacher t
where c.tid = t.tid
and t.tname = 'tw';
结果如下:
结果分析:有两个索引,c表的c.tid引用的是t表的tid字段,因此可以看到显示结果为【数据库名.t.tid】,t表的t.name引用的是一个常量"tw",因此可以看到结果显示为const,表示一个常量。
explain select *
from course c,teacher t
where c.tid = t.tid
and t.tname = 'tz' ;
Ⅰ 什么是“额外”的排序?
为了讲清楚这个,我们首先要知道什么是排序。我们为了给某一个字段进行排序的时候,首先你得先查询到这个字段,然后在将这个字段进行排序。
紧接着,我们查看如下两个SQL语句的执行计划。
# 新建一张表,建表同时创建索引
create table test02
(
a1 char(3),
a2 char(3),
a3 char(3),
index idx_a1(a1),
index idx_a2(a2),
index idx_a3(a3)
);
# 查看执行计划
explain select * from test02 where a1 ='' order by a1 ;
explain select * from test02 where a1 ='' order by a2 ;
结果如下:
结果分析:对于第一个执行计划,where后面我们先查询了a1字段,然后再利用a1做了依次排序,这个很轻松。但是对于第二个执行计划,where后面我们查询了a1字段,然而利用的却是a2字段进行排序,此时myql底层会进行一次查询,进行“额外”的排序。
总结:对于单索引,如果排序和查找是同一个字段,则不会出现using filesort;如果排序和查找不是同一个字段,则会出现using filesort;因此where哪些字段,就order by哪些些字段。
# 删除test02的索引
drop index idx_a1 on test02;
drop index idx_a2 on test02;
drop index idx_a3 on test02;
# 创建一个复合索引
alter table test02 add index idx_a1_a2_a3 (a1,a2,a3) ;
# 查看下面SQL语句的执行计划
explain select *from test02 where a1='' order by a3 ; --using filesort
explain select *from test02 where a2='' order by a3 ; --using filesort
explain select *from test02 where a1='' order by a2 ;
结果如下:
结果分析:复合索引的顺序是(a1,a2,a3),可以看到a1在最左边,因此a1就叫做“最佳左前缀”,如果要使用后面的索引字段,必须先使用到这个a1字段。对于explain1,where后面我们使用a1字段,但是后面的排序使用了a3,直接跳过了a2,属于跨列;对于explain2,where后面我们使用了a2字段,直接跳过了a1字段,也属于跨列;对于explain3,where后面我们使用a1字段,后面使用的是a2字段,因此没有出现【using filesort】。
explain select a1 from test02 where a1 in ('1','2','3') group by a1 ;
explain select a1 from test02 where a1 in ('1','2','3') group by a2 ; --using temporary
结果如下:
结果分析:当你查询哪个字段,就按照那个字段分组,否则就会出现using temporary。
针对using temporary,我们在看一个例子:
using temporary表示需要额外再使用一张表,一般出现在group by语句中。虽然已经有表了,但是不适用,必须再来一张表。
再次来看mysql的编写过程和解析过程。
Ⅰ 编写过程
select dinstinct ..from ..join ..on ..where ..group by ..having ..order by ..limit ..
Ⅱ 解析过程
from .. on.. join ..where ..group by ..having ..select dinstinct ..order by ..limit ..
很显然,where后是group by,然后才是select。基于此,我们再查看如下两个SQL语句的执行计划。
explain select * from test03 where a2=2 and a4=4 group by a2,a4;
explain select * from test03 where a2=2 and a4=4 group by a3;
分析如下:对于第一个执行计划,where后面是a2和a4,接着我们按照a2和a4分组,很明显这两张表已经有了,直接在a2和a4上分组就行了。但是对于第二个执行计划,where后面是a2和a4,接着我们却按照a3分组,很明显我们没有a3这张表,因此有需要再来一张临时表a3。因此就会出现using temporary。
# 删除test02中的复合索引idx_a1_a2_a3
drop index idx_a1_a2_a3 on test02;
# 重新创建一个复合索引idx_a1_a2
create index idx_a1_a2 on test02(a1,a2);
# 查看执行计划
explain select a1,a3 from test02 where a1='' or a3= '' ;
explain select a1,a2 from test02 where a1='' and a2= '' ;
结果如下:
结果分析:我们创建的是a1和a2的复合索引,对于第一个执行计划,我们却出现了a3,该字段并没有创建索引,因此没有出现using index,而是using where,表示我们需要回表查询。对于第二个执行计划,属于完全的索引覆盖,因此出现了using index。
针对using index,我们在查看一个案例:
explain select a1,a2 from test02 where a1='' or a2= '' ;
explain select a1,a2 from test02 ;
结果如下:
如果用到了索引覆盖(using index时),会对possible_keys和key造成影响:
# 删除test02中的复合索引idx_a1_a2
drop index idx_a1_a2 on test02;
# 将a1字段,新增为一个索引
create index a1_index on test02(a1);
# 查看执行计划
explain select a1,a3 from test02 where a1="" and a3="" ;
结果如下:
结果分析:我们既使用了索引a1,表示我们使用了索引进行查询。但是又对于a3字段,我们并没有使用索引,因此对于a3字段,需要回源表查询,这个时候出现了using where。
# 查看执行计划
explain select a1 from test02 where a1="a" and a1="b" ;
# 创建新表
create table test03
(
a1 int(4) not null,
a2 int(4) not null,
a3 int(4) not null,
a4 int(4) not null
);
# 创建一个复合索引
create index a1_a2_a3_test03 on test03(a1,a2,a3);
# 查看执行计划
explain select a3 from test03 where a1=1 and a2=2 and a3=3;
结果如下:
推荐写法:复合索引顺序和使用顺序一致。
下面看看【不推荐写法】:复合索引顺序和使用顺序不一致。
# 查看执行计划
explain select a3 from test03 where a3=1 and a2=2 and a1=3;
结果如下:
结果分析:虽然结果和上述结果一致,但是不推荐这样写。但是这样写怎么又没有问题呢?这是由于SQL优化器的功劳,它帮我们调整了顺序。
最后再补充一点:对于复合索引,不要跨列使用
# 查看执行计划
explain select a3 from test03 where a1=1 and a3=2 group by a3;
结果如下:
结果分析:a1_a2_a3是一个复合索引,我们使用a1索引后,直接跨列使用了a3,直接跳过索引a2,因此索引a3失效了,当使用a3进行分组的时候,就会出现using where。
# 创建新表
create table book
(
bid int(4) primary key,
name varchar(20) not null,
authorid int(4) not null,
publicid int(4) not null,
typeid int(4) not null
);
# 插入数据
insert into book values(1,'tjava',1,1,2) ;
insert into book values(2,'tc',2,1,2) ;
insert into book values(3,'wx',3,2,1) ;
insert into book values(4,'math',4,2,3) ;
结果如下:
案例:查询authorid=1且typeid为2或3的bid,并根据typeid降序排列。
explain
select bid from book
where typeid in(2,3) and authorid=1
order by typeid desc ;
结果如下:
这是没有进行任何优化的SQL,可以看到typ为ALL类型,extra为using filesort,可以想象这个SQL有多恐怖。
优化:添加索引的时候,要根据MySQL解析顺序添加索引,又回到了MySQL的解析顺序,下面我们再来看看MySQL的解析顺序。
from .. on.. join ..where ..group by ..having ..select dinstinct ..order by ..limit ..
# 添加索引
create index typeid_authorid_bid on book(typeid,authorid,bid);
# 再次查看执行计划
explain
select bid from book
where typeid in(2,3) and authorid=1
order by typeid desc ;
结果如下:
结果分析:结果并不是和我们想象的一样,还是出现了using where,查看索引长度key_len=8,表示我们只使用了2个索引,有一个索引失效了。
# 删除以前的索引
drop index typeid_authorid_bid on book;
# 再次创建索引
create index authorid_typeid_bid on book(authorid,typeid,bid);
# 再次查看执行计划
explain
select bid from book
where authorid=1 and typeid in(2,3)
order by typeid desc ;
结果如下:
结果分析:这里虽然没有变化,但是这是一种优化思路。
总结如下:
本例中同时出现了Using where(需要回原表); Using index(不需要回原表):原因,where authorid=1 and typeid in(2,3)中authorid在索引(authorid,typeid,bid)中,因此不需要回原表(直接在索引表中能查到);而typeid虽然也在索引(authorid,typeid,bid)中,但是含in的范围查询已经使该typeid索引失效,因此相当于没有typeid这个索引,所以需要回原表(using where);
例如以下没有了In,则不会出现using where:
explain select bid from book
where authorid=1 and typeid =3
order by typeid desc ;
# 创建teacher2新表
create table teacher2
(
tid int(4) primary key,
cid int(4) not null
);
# 插入数据
insert into teacher2 values(1,2);
insert into teacher2 values(2,1);
insert into teacher2 values(3,3);
# 创建course2新表
create table course2
(
cid int(4) ,
cname varchar(20)
);
# 插入数据
insert into course2 values(1,'java');
insert into course2 values(2,'python');
insert into course2 values(3,'kotlin');
案例:使用一个左连接,查找教java课程的所有信息。
explain
select *
from teacher2 t
left outer join course2 c
on t.cid=c.cid
where c.cname='java';
为什么小表驱动大表好一些呢?
小表:10
大表:300
# 小表驱动大表
select ...where 小表.x10=大表.x300 ;
for(int i=0;i<小表.length10;i++)
{
for(int j=0;j<大表.length300;j++)
{
...
}
}
# 大表驱动小表
select ...where 大表.x300=小表.x10 ;
for(int i=0;i<大表.length300;i++)
{
for(int j=0;j<小表.length10;j++)
{
...
}
}
分析:以上2个FOR循环,最终都会循环3000次;但是对于双层循环来说:一般建议,将数据小的循环,放外层。数据大的循环,放内层。不用管这是为什么,这是编程语言的一个原则,对于双重循环,外层循环少,内存循环大,程序的性能越高。
结论:当编写【…on t.cid=c.cid】时,将数据量小的表放左边(假设此时t表数据量小,c表数据量大。)
我们已经知道了,对于两表连接,需要利用小表驱动大表,例如【…on t.cid=c.cid】,t如果是小表(10条),c如果是大表(300条),那么t每循环1次,就需要循环300次,即t表的t.cid字段属于,经常使用的字段,因此需要给cid字段添加索引。
更深入的说明:一般情况下,左连接给左表加索引。右连接给右表加索引。其他表需不需要加索引,我们逐步尝试。
# 给左表的字段加索引
create index cid_teacher2 on teacher2(cid);
# 查看执行计划
explain
select *
from teacher2 t
left outer join course2 c
on t.cid=c.cid
where c.cname='java';
结果如下:
当然你可以下去接着优化,给cname添加一个索引。索引优化是一个逐步的过程,需要一点点尝试。
# 给cname的字段加索引
create index cname_course2 on course2(cname);
# 查看执行计划
explain
select t.cid,c.cname
from teacher2 t
left outer join course2 c
on t.cid=c.cid
where c.cname='java';
结果如下:
最后补充一个:Using join buffer是extra中的一个选项,表示Mysql引擎使用了“连接缓存”,即MySQL底层动了你的SQL,你写的太差了。
explain select * from book where authorid = 1 and typeid = 2;
explain select * from book where authorid*2 = 1 and typeid = 2 ;
# 针对不是复合索引的情况
explain select * from book where authorid != 1 and typeid =2 ;
explain select * from book where authorid != 1 and typeid !=2 ;
# 删除单独的索引
drop index authorid_index on book;
drop index typeid_index on book;
# 创建一个复合索引
alter table book add index idx_book_at (authorid,typeid);
# 查看执行计划
explain select * from book where authorid > 1 and typeid = 2 ;
explain select * from book where authorid = 1 and typeid > 2 ;
结果如下:
结论:复合索引中如果有【>】,则自身和右侧索引全部失效。
在看看复合索引中有【<】的情况:
我们学习索引优化 ,是一个大部分情况适用的结论,但由于SQL优化器等原因 该结论不是100%正确。一般而言, 范围查询(> < in),之后的索引失效。
# 删除复合索引
drop index authorid_typeid_bid on book;
# 为authorid和typeid,分别创建索引
create index authorid_index on book(authorid);
create index typeid_index on book(typeid);
# 查看执行计划
explain select * from book where authorid = 1 and typeid =2 ;
结果如下:
结果分析:我们创建了两个索引,但是实际上只使用了一个索引。因为对于两个单独的索引,程序觉得只用一个索引就够了,不需要使用两个。
当我们创建一个复合索引,再次执行上面的SQL:
# 查看执行计划
explain select * from book where authorid = 1 and typeid =2 ;
explain select * from teacher where tname like "%x%" ;
explain select * from teacher where tname like 'x%';
explain select tname from teacher where tname like '%x%';
结果如下:
结论如下:like尽量不要使用类似"%x%"情况,但是可以使用"x%"情况。如果非使用 "%x%"情况,需要使用索引覆盖。
explain select * from teacher where tname = 'abc' ;
explain select * from teacher where tname = 123 ;
explain select * from teacher where tname ='' and tcid >1 ;
explain select * from teacher where tname ='' or tcid >1 ;
结果如下:
注意:or很猛,会让自身索引和左右两侧的索引都失效。
select ..from table where exist (子查询) ;
select ..from table where 字段 in (子查询) ;
# 不一定真的是“单路/1次IO”,有可能多次IO
set max_length_for_sort_data = 1024
如果max_length_for_sort_data值太低,则mysql会自动从 单路->双路(太低:需要排序的列的总大小超过了max_length_for_sort_data定义的字节数)