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 来查看排序的结果:
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 排序多访问了一次表 t 的主键索引。
需要说明的是,最后的“结果集”是一个逻辑概念,实际上 MySQL 服务端从排序后的
sort_buffer 中依次取出 id,然后到原表查到 city、name 和 age 这三个字段的结果,不
需要在服务端再耗费内存存储结果,是直接返回给客户端的。
sort_mode 变成了,表示参与排序的只有 name 和 id 这两个字段。
number_of_tmp_files 变成 10 了,是因为这时候参与排序的行数虽然仍然是 4000
行,但是每一行都变小了,因此需要排序的总数据量就变小了,需要的临时文件也相应
地变少了。
3. 全字段排序 VS rowid 排序
如果 MySQL 实在是担心排序内存太小,会影响排序效率,才会采用 rowid 排序算法,这
样排序过程中一次可以排序更多行,但是需要再回到原表去取数据。
如果 MySQL 认为内存足够大,会优先选择全字段排序,把需要的字段都放到 sort_buffer
中,这样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据。
这也就体现了 MySQL 的一个设计思想:如果内存够,就要多利用内存,尽量减少磁盘访
问。
对于 InnoDB 表来说,rowid 排序会要求回表多造成磁盘读,因此不会被优先选择。
如果上面的例子建立(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排序不是一回事。