(7)order by与随机排序

假设有这么一个业务需求,你要查询城市是 “ 杭州 ” 的所有人名字,并且按照姓名排序返回前 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这个索引的示意图

(7)order by与随机排序_第1张图片

从图中可以看到,满足 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 行返回给客户端。

(7)order by与随机排序_第2张图片

在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 字段

二、rowid排序

在上面这个算法过程里面,只对原表的数据读了一遍,剩下的操作都是在 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 三个字段返回给客户端。

(7)order by与随机排序_第3张图片

 rowid 排序多访问了一次表 t 的主键索引,也就是多了回表过程。就是步骤 7 。

接下来,我们看两种使用row _id 排序的语句

三、 order by rand() 的临时表排序

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 命令来看看这个语句的执行情况。

  •  Using temporary ,表示的是需要使用临时表
  •  Using filesort ,表示的是需要执行排序操作。即在临时表上排序

对于 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 。

(7)order by与随机排序_第4张图片

这个位置信息pos,就是rowid。实际上它表示的是:每个引擎用来唯一标识数据行的信息。

  • 对于有主键的 InnoDB 表来说,这个 rowid 就是主键 ID ;
  • 创建的 InnoDB 表没有指定主键,那么 InnoDB 会给你创建一个不可见的,长度为 6 个字节的 row_id
  • MEMORY 引擎不是索引组织表。在这个例子里面,你可以认为它就是一个数组。因此,这个rowid 其实就是数组的下标。

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

 

四、GROUP BY

执行下面的语句

select id%10 as m, count(*) as c from t1 group by m;

这个语句的逻辑是把表 t1 里的数据,按照 id%10  进行分组统计(结果为0,1,2,到9),并按照 m 的结果排序后输出。它的 explain 结果如下

在 Extra 字段里面,我们可以看到三个信息:

  • Using index ,表示这个语句使用了覆盖索引,选择了索引 a ,不需要回表;
  • Using temporary ,表示使用了临时表;
  • Using filesort ,表示需要排序。

这个语句的执行流程是这样的:
1.  创建内存临时表,表里有两个字段 m 和 c ,主键是 m ;
2.  扫描表 t1 的索引 a ,依次取出叶子节点上的 id 值,计算 id%10 的结果,记为 x ;

  • 如果临时表中没有主键为 x 的行,就插入一个记录 (x,1);
  • 如果表中有主键为 x 的行,就将 x 这一行的 c 值加 1 ;

3.  遍历完成后,再根据字段 m 做排序(row_id排序),得到结果集返回给客户端。

小结:group by排序使用了内存临时表,内存临时表排序的时候使用了 rowid 排序方法。

(7)order by与随机排序_第5张图片

 

 

 

五、order by的优化

如果 MySQL 认为内存足够大,会优先选择全字段排序(innodb表情况下),这样排序后直接会从内存中返回查询结果;

如果 MySQL 实在是担心排序内存太小,会影响排序效率,才会采用 rowid 排序算法,但是要再回原表去数据。

注意,这是针对innodb表来说的优先选择全字段排序,针对memory引擎表,情况则不同

这也就体现了 MySQL 的一个设计思想: 如果内存够,就要多利用内存,尽量减少磁盘访问。 rowid 排序会要求回表多造成磁盘读,因此不会被优先选择。


 

其实,并不是所有的 order by 语句,都需要排序操作的。

从上面分析的执行过程,我们可以看到,

  • MySQL 之所以需要生成sort_buffer,并且在上面做排序操作, 其原因是原来的数据都是无序的。如果我们能保证从索引上取去来的行,天然已经递增排序了,那么就不用在排序了。
  • 因为我们想查询的是city name和age,其中不包括主键id,利用覆盖索引,从city索引上直接取得其他两个字段的值。可以省去回表的操作

可以创建一个 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=' 杭州 ’ 条件时循环结束。

(7)order by与随机排序_第6张图片

这个查询过程不需要临时表,也不需要排序,也不需要回表

一个特例:

 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 值,就是我们需要的结果了。

 

 

 

 

 

你可能感兴趣的:((7)order by与随机排序)