假设有这么一个业务需求,你要查询城市是 “ 杭州 ” 的所有人名字,并且按照姓名排序返回前 1000 个人的姓名、年龄,SQL语句是这么写的
select city,name,age from t where city=' 杭州 ' order by name limit 1000
前面我们说过,MySQL有两种排序方式:文件排序(FileSort)或者扫描有序索引排序(index)。我们应该尽量使用索引排序,故下面两种方式都是在索引排序的基础上实现的
上面这个句子where的查询条件是city,为避免全表扫描,我们需要在 city 字段加上索引。
在 city 字段上创建索引之后,我们用 explain 命令来看看这个语句的执行情况
Extra 这个字段中的 “Using filesort” 表示的就是需要排序, MySQL 会给每个线程分配一块内存用于排序,称为 sort_buffer 。
如下图所示为city这个索引的示意图
从图中可以看到,满足 city=' 杭州 ’ 条件的行,是从 ID_X 到 ID_(X+N) 的这些记录。
通常情况下,这个语句执行流程如下所示 :
1. 初始化 sort_buffer ,确定放入 name 、 city 、 age 这三个字段(返回所需的字段值)
2. 从索引 city 找到第一个满足 city=' 杭州 ’ 条件的主键 id ,也就是图中的 ID_X ;
3. 到主键 id 索引取出整行,取 name 、 city 、 age 三个字段的值(回表过程),存入 sort_buffer 中;
4. 从索引 city 取下一个记录的主键 id ;
5. 重复步骤 3 、 4 直到 city 的值不满足查询条件为止,对应的主键 id 也就是图中的 ID_Y ;
6. 对 sort_buffer 中的数据按照字段 name 做快速排序;
7. 按照排序结果取前 1000 行返回给客户端。
在sort_buffer中的排序,可能在内存中完成,也可能需要使用外部排序,这取决于排序所需的内存和参数 sort_buffer_size 的大小
如果排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序: MySQL 将需要排序的数据分成 多份,每一份单独排序后存在这些临时文件中。然后把这 这么多个有序文件再合并成一个有序的大文件
注意一个特例:(8)group by和order by rand的优化中,order by rand 中用row id排序算法,sort_buffer数据总量超过sort_buffer_size时,使用的是优先队列算法,来获得最小的 (R,rowid)行,在把他们的 rowid 取出来,去内存临时表里面拿到 word 字段
在上面这个算法过程里面,只对原表的数据读了一遍,剩下的操作都是在 sort_buffer 或者临时文件中执行的。如果查询要返回的字段很多的话,那么 sort_buffer 里面要放的字段数太多,这样内存里能够同时放下的行数很少,要分成很多个临时文件,排序性能变差
也就是说,如果单行很大,这个方法效率不够好。
可以修改一个参数,让 MySQL 采用另外一种算法
SET max_length_for_sort_data = 16;
max_length_for_sort_data ,是 MySQL 中专门控制用于排序的行数据的长度的一个参数。如果单行长度超过这个值,那么放入sort_buffer的字段数会被截断
city 、 name 、 age 这三个字段的定义总长度是 36 ,我把 max_length_for_sort_data 设置为 16,新的算法放入 sort_buffer 的字段,只有要排序的列(即 name 字段)和主键 id 。那么sort_buffer还缺几个字段的值,就不能从中直接返回结果了
下面是执行流程:
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 的主键索引,也就是多了回表过程。就是步骤 7 。
接下来,我们看两种使用row _id 排序的语句
CREATE TABLE `words` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`word` varchar(64) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
我在这个表里面插入了 10000 行记录。接下来,我们就一起看看要随机选择3 个单词。首先,会想到用 order by rand() 来实现这个逻辑:随机取前三个
select word from words order by rand() limit 3;
用 explain 命令来看看这个语句的执行情况。
对于 InnoDB 表来说,执行全字段排序会减少磁盘访问,因此会被优先选择。
对于内存表,回表过程只是简单地根据数据行的位置,直接访问内存得到数据,根本不会导致多访问磁盘。
所以,对于内存临时表的排序, MySQL 这时就会选择 rowid 排序。
加入words表中有一万条数据,这条语句的执行流程是这样的:
1. 创建一个临时表。这个临时表使用的是 memory 引擎(内存临时表),表里有两个字段,第一个字段是double 类型,为了后面描述方便,记为字段 R ,第二个字段是 varchar(64) 类型,记为字段W 。并且,这个内存临时表没有建索引。
2. 从 words 表中,按主键顺序取出所有的 word 值。对于每一个 word 值,调用 rand() 函数生成一个大于 0 小于 1 的随机小数,并把这个随机小数和 word 分别存入临时表的 R 和 W 字段中。
3. 现在临时表有 10000 行数据了,接下来你要在这个没有索引的内存临时表上,按照字段 R (随机小数)排序。
4.初始化 sort_buffer 。 sort_buffer 中有两个字段,一个是 double 类型,另一个是整型。
5. 从内存临时表中一行一行地取出 R 值和位置信息pos(我后面会和你解释这里为什么是 “ 位置信息 ” ),分别存入 sort_buffer 中的两个字段里。这个过程要对内存临时表做全表扫描,此时扫描行数增加 10000 ,变成了 20000 。
6. 在 sort_buffer 中根据 R 的值进行排序。注意,这个过程没有涉及到表操作,所以不会增加扫描行数。
7. 排序完成后,取出前三个结果的位置信息,依次到内存临时表中取出 word 值,返回给客户端。这个过程中,访问了表的三行数据,总扫描行数变成了 20003 。
这个位置信息pos,就是rowid。实际上它表示的是:每个引擎用来唯一标识数据行的信息。
- 对于有主键的 InnoDB 表来说,这个 rowid 就是主键 ID ;
- 创建的 InnoDB 表没有指定主键,那么 InnoDB 会给你创建一个不可见的,长度为 6 个字节的 row_id
- MEMORY 引擎不是索引组织表。在这个例子里面,你可以认为它就是一个数组。因此,这个rowid 其实就是数组的下标。
order by rand() 使用了内存临时表,内存临时表排序的时候使用了 rowid 排序方法。
执行下面的语句
select id%10 as m, count(*) as c from t1 group by m;
这个语句的逻辑是把表 t1 里的数据,按照 id%10 进行分组统计(结果为0,1,2,到9),并按照 m 的结果排序后输出。它的 explain 结果如下
在 Extra 字段里面,我们可以看到三个信息:
这个语句的执行流程是这样的:
1. 创建内存临时表,表里有两个字段 m 和 c ,主键是 m ;
2. 扫描表 t1 的索引 a ,依次取出叶子节点上的 id 值,计算 id%10 的结果,记为 x ;
3. 遍历完成后,再根据字段 m 做排序(row_id排序),得到结果集返回给客户端。
小结:group by排序使用了内存临时表,内存临时表排序的时候使用了 rowid 排序方法。
如果 MySQL 认为内存足够大,会优先选择全字段排序(innodb表情况下),这样排序后直接会从内存中返回查询结果;
如果 MySQL 实在是担心排序内存太小,会影响排序效率,才会采用 rowid 排序算法,但是要再回原表去数据。
注意,这是针对innodb表来说的优先选择全字段排序,针对memory引擎表,情况则不同
这也就体现了 MySQL 的一个设计思想: 如果内存够,就要多利用内存,尽量减少磁盘访问。 rowid 排序会要求回表多造成磁盘读,因此不会被优先选择。
其实,并不是所有的 order by 语句,都需要排序操作的。
从上面分析的执行过程,我们可以看到,
可以创建一个 city 、 name 和 age 的联合索引,对应的 SQL 语句就是:
alter table t add index city_user_age(city, name, age);
这时,对于 city 字段的值相同的行来说,还是按照 name 字段的值递增排序的,这就是联合索引的有序性
1. 从索引 (city,name,age) 找到第一个满足 city=' 杭州 ’ 条件的记录,取出其中的 city 、 name 和 age这三个字段的值,它们一定是天然有序的,作为结果集的一部分直接返回;
2. 从索引 (city,name,age) 取下一个记录,同样取出这三个字段的值,作为结果集的一部分直接返回;
3. 重复执行步骤 2 ,直到查到第 1000 条记录,或者是不满足 city=' 杭州 ’ 条件时循环结束。
这个查询过程不需要临时表,也不需要排序,也不需要回表
一个特例:
select * from t where city in (“ 杭州 ”," 苏州 ") order by name limit 100
虽然有 (city,name) 联合索引,对于单个 city 内部, name 是递增的。这条 SQL 语句不是要单独地查一个 city 的值,而是同时查了 " 杭州 " 和 " 苏州 " 两个城市,因此所有满足条件的 name 就不是递增的了。也就是说,这条 SQL 语句需要排序
如何避免排序?
这里,我们要用到 (city,name) 联合索引的特性,把这一条语句拆成两条语句,执行流程如下:
1. 执行 select * from t where city=“ 杭州 ” order by name limit 100; 这个语句是不需要排序的,客户端用一个长度为 100 的内存数组 A 保存结果。
2. 执行 select * from t where city=“ 苏州 ” order by name limit 100; 用相同的方法,假设结果被存进了内存数组 B 。
3. 现在 A 和 B 是两个有序数组,然后你可以用归并排序的思想,得到 name 最小的前 100 值,就是我们需要的结果了。