create table a(
city varchar(16) not null,
name varchar(16) not null,
id int(11) not null,
primary key(id)
)
一、全字段排序
比如在执行一个
select city, name, ID from T
where city='广州'
order by name limit 200;
city字段有索引(用来快速过滤出 “广州” 的记录)
-
name字段没有索引(无法直接按 name 有序返回)触发全字段排序,mysql就会把需要返回的字段(city,name,UID)放到一个 sort-buffer的内存,按这个内存块的name排序。
流程
1、先利用索引过滤
先通过索引 city,找到city= 杭州对应的主键ID。
2、根据主键回表取返回字段
根据主键ID,读取整行数据,只取需要返回的字段city,name,id,
放入内存块sort-buffer
3、在sort-buffer排序
sort_buffer 是 MySQL 为每个线程分配的一块排序专用内存
如果 sort_buffer 能装下所有待排序数据就直接排序。
如果数据太多,就会把数据分成多份,每份排序后存到临时磁盘文件中,把这些有序文件合并成一个大的有序结果
4、返回结果
二、rowid排序
触发条件:当单行数据长度超过 max_length_for_sort_data 参数时,MySQL 会认为单行太大,不适合把所有返回字段都塞进 sort_buffer,而改用 rowid 排序:
注:max_length_for_sort_data是用户自己设置。
set max_length_for_sort_data = 20;
流程
把需要排序的字段(如name)和主键ID放入sort-buffer
排序完成后,再根据排序后的ID回表提取其他需要返回的字段(city,name,id)。
三、为什么需要排序?
在只有 city 索引的情况下:
city索引只保证了city字段的有序性,但满足city='杭州'的行,name字段是无序的。- 因此
order by name必须通过排序(全字段或 rowid)来实现,
四、全字段排序 vs rowid 排序
全字段排序
原理(简):把需要返回的字段在sort-buffer排序。
pre:无需回表,一次读取
pro: 单行数据大时,sort_buffer 很快占满,需依赖磁盘临时文件,性能差
rowid排序
原理(简):只把order by的字段和主键ID在sort-buffer排序,再回表提取其他返回字段。
pre:内存占用小,减少临时文件数量
pro: 多一次回表操作,增加 IO 开销
需要采用全字段排序和rowid排序是因为原数据是无序的。
五、联合索引
如果在city索引取的行是按照name递增排序,就不用做排序了(全字段排序ORrowid排序)。
所以可以在创建表后,表外添加索引,创建一个city和name的联合索引,
alter table a add index city_user(city,name);
当我们创建 (city, name) 联合索引后: 索引的结构是先按 city 排序,再按 name 排序。
———— city='广州' 的行,因为name 天然是有序的。 所以此时 order by name 不再需要排序,直接按索引顺序读取即可,性能大幅提升。
流程
从 (city, name) 索引找到第一个 city='广州' 的行,获取主键 id。
回表到主键索引,取出 id 字段,与索引中的 city, name 一起返回。
按索引顺序取下一行,重复步骤 2,直到取够 1000 行。
六、覆盖索引
如果我们进一步创建 (city, name, di) 覆盖索引:
这样的话,索引中就包含了查询的所有需要返回的字段,就不用回表操作,直接读取索引数据。
输出explain 结果中的 Extra 字段显示 Using index,是覆盖索引的标志,表示性能达到最优。
alter table a add index city_user(city,name,id);
流程
- 从
(city, name, age)索引找到第一个city='杭州'的行,直接取出city,name,age返回。 - 按索引顺序取下一行,重复步骤 1,直到取够 1000 行。
结论
排序是成本很高的操作,所以能通过索引避免就尽量避免。
覆盖索引是性能优化的最优手段,但需要权衡索引的维护成本,和查询收益,不是所有场景都适合。