1.全字段排序

CREATE TABLE `t` (
`id` int(11) NOT NULL,
`city` varchar(16) NOT NULL,
`name` varchar(16) NOT NULL,
`age` int(11) NOT NULL,
`addr` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `city` (`city`)
) ENGINE=InnoDB;
select city,name,age from t where city='杭州' order by name limit 1000 ;

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 的值不满足查询条件为止,对应的主键 id

6)对 sort_buffer 中的数据按照字段 name 做快速排序;

7)按照排序结果取前 1000 行返回给客户端。

全字段排序

图中“按 name 排序”这个动作,可能在内存中完成,也可能需要使用外部排序,这取决

于排序所需的内存和参数 sort_buffer_size。

sort_buffer_size,就是 MySQL 为排序开辟的内存(sort_buffer)的大小。如果要排序

的数据量小于 sort_buffer_size,排序就在内存中完成。但如果排序数据量太大,内存放

不下,则不得不利用磁盘临时文件辅助排序。

通过查看 OPTIMIZER_TRACE 来查看排序的结果:

全排序的 OPTIMIZER_TRACE 部分结果

number_of_tmp_files 表示的是,排序过程中使用的临时文件数。你一定奇怪,为什么需

要 12 个文件?内存放不下时,就需要使用外部排序,外部排序一般使用归并排序算法。

可以这么简单理解,MySQL 将需要排序的数据分成 12 份,每一份单独排序后存在这些临

时文件中。然后把这 12 个有序文件再合并成一个有序的大文件。

如果 sort_buffer_size 超过了需要排序的数据量的大小,number_of_tmp_files 就是 0,

表示排序可以直接在内存中完成。

否则就需要放在临时文件中排序。sort_buffer_size 越小,需要分成的份数越多,

number_of_tmp_files 的值就越大。

examined_rows=4000,表示参与排序的行数是 4000 行。

结论:

只对原表的数据读了一遍,剩下的操作都是在 sort_buffer 和

临时文件中执行的。但这个算法有一个问题,就是如果查询要返回的字段很多的话,那么

sort_buffer 里面要放的字段数太多,这样内存里能够同时放下的行数很少,要分成很多个

临时文件,排序的性能会很差。

所以如果单行很大,这个方法效率不够好。

2. rowid 排序

如果 MySQL 认为排序的单行长度太大会怎么做呢?

SET max_length_for_sort_data = 16;

max_length_for_sort_data,是 MySQL 中专门控制用于排序的行数据的长度的一个参

数。它的意思是,如果单行的长度超过这个值,MySQL 就认为单行太大,要换一个算

法。

新的算法(rowid)放入 sort_buffer 的字段,只有要排序的列(即 name 字段)和主键 id。

rowid排序的流程:

1. 初始化 sort_buffer,确定放入两个字段,即 name 和 id;

2. 从索引 city 找到第一个满足 city='杭州’条件的主键 id,也就是图中的 ID_X;

3. 到主键 id 索引取出整行,取 name、id 这两个字段,存入 sort_buffer 中;

4. 从索引 city 取下一个记录的主键 id;

5. 重复步骤 3、4 直到不满足 city='杭州’条件为止,也就是图中的 ID_Y;

6. 对 sort_buffer 中的数据按照字段 name 进行排序;

7. 遍历排序结果,取前 1000 行,并按照 id 的值回到原表中取出 city、name 和 age 三

个字段返回给客户端。

rowid 排序

对比全字段排序流程图会发现,rowid 排序多访问了一次表 t 的主键索引。

需要说明的是,最后的“结果集”是一个逻辑概念,实际上 MySQL 服务端从排序后的

sort_buffer 中依次取出 id,然后到原表查到 city、name 和 age 这三个字段的结果,不

需要在服务端再耗费内存存储结果,是直接返回给客户端的。

rowid 排序的 OPTIMIZER_TRACE 部分输出

sort_mode 变成了,表示参与排序的只有 name 和 id 这两个字段。

number_of_tmp_files 变成 10 了,是因为这时候参与排序的行数虽然仍然是 4000

行,但是每一行都变小了,因此需要排序的总数据量就变小了,需要的临时文件也相应

地变少了。

3. 全字段排序 VS rowid 排序

如果 MySQL 实在是担心排序内存太小,会影响排序效率,才会采用 rowid 排序算法,这

样排序过程中一次可以排序更多行,但是需要再回到原表去取数据。

如果 MySQL 认为内存足够大,会优先选择全字段排序,把需要的字段都放到 sort_buffer

中,这样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据。

这也就体现了 MySQL 的一个设计思想:如果内存够,就要多利用内存,尽量减少磁盘访

问。

对于 InnoDB 表来说,rowid 排序会要求回表多造成磁盘读,因此不会被优先选择。

如果上面的例子建立(city, name)的联合索引,本身就是已经排好序的。

引入 (city,name) 联合索引后,查询语句的执行计划

如果建立的是覆盖索引,则效率会更高。

MySQL 的表是用什么方法来定位“一行数据”的?

创建的表没有主键,或者把一个表的主键删掉了,那么 InnoDB 会自己生成一个长度为 6 字节的 rowid 来作为主键。

这也就是排序模式里面,rowid 名字的来历。实际上它表示的是:每个引擎用来唯一标识数据行的信息。

对于有主键的 InnoDB 表来说,这个 rowid 就是主键 ID;

对于没有主键的 InnoDB 表来说,这个 rowid 就是由系统生成的;

order by rand() 使用了内存临时表,内存临时表排序的时候使用了 rowid 排序方法。

磁盘临时表

tmp_table_size 这个配置限制了内存临时表的大小,默认值是 16M。

如果临时表大小超过了 tmp_table_size,那么内存临时表就会转成磁盘临时表。

磁盘临时表使用的引擎默认是 InnoDB,是由参数 internal_tmp_disk_storage_engine

控制的。

MySQL 5.6 版本引入的一个新的排序算法,即:优先队列排序算法。

什么时候采用优先队列排序算法?

当返回的数据结果集小于sort_buffer_size的大小时,采用优先队列排序算法,此时,不需要临时文件,所以,number_of_tmp_files = 0

什么时候采用用临时文件的算法,也就是归并排序算法?

比如返回的结果 limit 10000,超过了sort_buffer_size的大小,只能采用归并排序算法。

说明:优先队列排序算法与归并算法是结果集的排序算法,跟全字段排序与rowid排序不是一回事。