order by 是怎么工作的?
select city,name,age from t where city='杭州' order by name limit 1000 ;
对于这个排序语句来说,我们通常会给city加上索引,然后我们通过explain命令来查看该语句的执行情况。
其中Extra字段中的Using filesort表示需要排序,MySQL会给每个线程分配一块内存用于排序,称为sort_buffer。
该语句的执行顺序:
1.初始化 sort_buffer,确定放入 name、city、age 这三个字段;
2.从索引 city 找到第一个满足 city='杭州’条件的主键 id;
3.到主键 id 索引取出整行,取 name、city、age 三个字段的值,存入 sort_buffer 中;
4.从索引 city 取下一个记录的主键 id;
5.重复步骤 3、4 直到 city 的值不满足查询条件为止;
6.对 sort_buffer 中的数据按照字段 name 做快速排序;
7.按照排序结果取前 1000 行返回给客户端。
我们把这个排序过程,成为全字段排序。
按照name排序这个动作,可能在内存中完成,也有可能使用外部排序,这取决于所遇内存和参数sort_buffer_size。
sort_buffer_size,就是 MySQL 为排序开辟的内存(sort_buffer)的大小。如果要排序的数据量小于 sort_buffer_size,排序就在内存中完成。但如果排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。
我们可以通过查看OPTIMIZER_TRACE来查看是否使用了临时文件: SELECT * FROM infornation_schema.OPTIMIZER_TRACE\G
执行后我们可以看到
number_of_tmp_files表示是否使用了临时文件和使用的个数
examined_rows表示参与排序的行数
sort_mode表示排序的字段
当查询的字段有很多时,sort_buffer里要放的字段太多,内存里放下的行数很少,要分成很多个临时文件,排序的性能会变差。这时我们可以让MySQL采用另一种算法,我们先设置:SET max_length_for_sort_data = 16;
max_length_for_sort_data表示如果单行的长度超过了这个值,就要换另一种算法。
这时我们再去执行排序语句,由于长度小了,放入sort_buffer的只有排序的列(即name字段)和主键id
这时执行流程如下:
1.初始化 sort_buffer,确定放入两个字段,即 name 和 id;
2.从索引 city 找到第一个满足 city='杭州’条件的主键 id,也就是图中的 ID_X;
3.到主键 id 索引取出整行,取 name、id 这两个字段,存入 sort_buffer 中;
4.从索引 city 取下一个记录的主键 id;重复步骤 3、4 直到不满足 city='杭州’条件为止,也就是图中的 ID_Y;
5.对 sort_buffer 中的数据按照字段 name 进行排序;
6.遍历排序结果,取前 1000 行,并按照 id 的值回到原表中取出 city、name 和 age 三个字段返回给客户端。
这个执行流程的示意图如下,我把它称为 rowid 排序。
相比于全字段排序,rowId排序多了一次回表的流程,但是由于sort_buffer变小了,所需要的临时文件也变小少了。
这两种排序方式得出结论:
如果 MySQL 实在是担心排序内存太小,会影响排序效率,才会采用 rowid 排序算法,这样排序过程中一次可以排序更多行,但是需要再回到原表去取数据。
如果 MySQL 认为内存足够大,会优先选择全字段排序,把需要的字段都放到 sort_buffer 中,这样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据。
这也就体现了 MySQL 的一个设计思想:如果内存够,就要多利用内存,尽量减少磁盘访问。
是不是所有order by 都需要排序呢?
其实并不是,我们上面的操作之所以会产生临时表,是因为要用来做排序操作,如果我们查询出来的数据本来就是有序的,那就不需要再排序了。
我们可以给要判断的条件的字段和order by的字段设置联合索引
alter table t add index city_user(city, name);
这样查出来的数据本来就是有序的了,这样就只需在做一次回表得到结果集就可以了。
甚至我们可以创建一个 city、name 和 age 的联合索引
alter table t add index city_user_age(city, name, age);
这样都不需要回表,直接取出这三个字段的值作为结果集直接返回。