(8)group by和order by rand随机排序的优化

在(7)使用索引排序过程,我介绍了几种排序的机制,以及使用排序的部分SQL语句,其中包括group by和order by rand()。

下面,我们可以看一下如何优化这两种SQL语句的效率

一、order by rand() 的磁盘临时表与优先队列算法

(7)使用索引排序过程中介绍了order by rand()语句,在第一个步骤中使用了内存临时表, (因为是memory引擎优先使用row id 排序)使用了 row id 排序方法,把内存临时表的数据放入sort_buffer进行排序

若一开始建立内存临时表时总数据量过大,则会转成磁盘临时表

  tmp_table_size 这个配置限制了内存临时表的大小,默认值是 16M。如果临时表大小超过了 tmp_table_size ,那么内存临时表就会转成磁盘临时表。

磁盘临时表使用的引擎默认是 InnoDB,当使用磁盘临时表的时候,对应的就是一个没有显式索引的 InnoDB 表的排序过程(回顾orderby的排序机制)


我把 tmp_table_size 设置成 1024 ,把 sort_buffer_size 设置成 32768,  把max_length_for_sort_data  设置成 16 ,复现以下这个过程,还是执行这个语句

select word from words order by rand() limit 3;

查看 OPTIMIZER_TRACE  输出:

(8)group by和order by rand随机排序的优化_第1张图片

从这个结果可以看出:

  •  max_length_for_sort_data 设置成 16 ,小于 word 字段的长度定义,sort_mode 里面显示的是 rowid 排序,这个是符合预期的,参与排序的是随机值 R 字段和 rowid 字段组成的行。
  •  R 字段存放的随机值就 8 个字节, rowid 是 6 个字节,数据总行数是 10000 ,这样算出来就有 140000 字节,超过了 sort_buffer_size  定义的 32768 字节了。但是, number_of_tmp_files 的值居然是 0 

这个 SQL 语句的排序确实没有用到临时文件,即没有用到归并排序算法,采用是 MySQL 5.6 版本引入的一个新的排序算法,即:优先队列排序算法。

如果使用归并排序算法的话,只需要取 R 值最小的 3 个 rowid,虽然最终也能得到前 3 个值,但是这个算法结束后,已经将 10000 行数据都排好序了,后面9997排序浪费了

而优先队列算法,就可以精确地只得到三个最小值

执行流程如下:

1.  对于这 10000 个准备排序的 (R,rowid) ,先取前三行,构造成一个堆

2.  取下一个行 (R’,rowid’) ,跟当前堆里面最大的 R 比较,如果 R’ 小于 R ,把这个 (R,rowid) 从堆中去掉,换成 (R’,rowid’) ;
3.  重复第 2 步,直到第 10000 个 (R’,rowid’) 完成比较。

(8)group by和order by rand随机排序的优化_第2张图片

整个排序过程中,为了最快地拿到当前堆的最大值,总是保持最大值在堆顶,因此这是一个最大堆。

图中的 OPTIMIZER_TRACE 结果中, filesort_priority_queue_optimization 这个部分的chosen=true ,就表示使用了优先队列排序算法,这个过程不需要临时文件,因此对应的number_of_tmp_files 是 0 。

这个流程结束后,我们构造的堆里面,就是这个 10000 行里面 R 值最小的三行(rand函数生成的随机小数的最小的三行)。然后,依次把它们的 rowid 取出来,去内存(或磁盘)临时表里面拿到 word 字段,这个过程就跟上一篇文章的 rowid 排序的过程一样了。

如果我们的SQL语句limit 1000 ,如果使用优先队列算法的话,需要维护的堆的大小就是 1000 行的 (name,rowid) ,超
过了我设置的 sort_buffer_size 大小,所以只能使用归并排序算法。

小结:

  • order by rand语句第一步建立内存临时表时,因为有大小限制,所以该内存临时表可能转为磁盘临时表
  •  (R,rowid)放入sort_buffer排序时,若总数据量没有超过sort_buffer_size,使用了优先队列算法
  • 如果需要维护的堆的大小超过sort_buffer_size,只能使用外部文件归并排序

二、优化的随机排序算法

 

从(7)order by与随机排序和上面的内容来看。不论是使用哪种类型的临时表, order by rand() 这种写法都会让计算过程非常复杂

我们提出了一种优化的方法,如果只随机选择一个word值,可以由如下的流程:

1.  取得整个表的行数,并记为 C 。
2.  取得 Y = floor(C * rand()) 。 floor 函数在这里的作用,就是取整数部分。
3.  再用 limit Y,1  取得一行。  其中Y作为步长,1作为返回记录行的数目

select count(*) into @C from t;
set @Y = floor(@C * rand());
set @sql = concat("select * from t limit ", @Y, ",1");
prepare stmt from @sql;
execute stmt;
DEALLOCATE prepare stmt;

由于 limit  后面的参数不能直接跟变量,所以我在上面的代码中使用了 prepare+execute 的方法

MySQL 处理 limit Y,1  的做法就是按顺序一个一个地读出来,丢掉前 Y 个,然后把下一个记录作为返回结果,因此这一步需要扫描 Y+1 行,再加上,第一步扫描的 C 行,总共需要扫描 C+Y+1 行。

代价比order by rand代价小很多:进行 limit 获取数据的时候是根据主键排序获取的,主键天然索引排序。获取到第9999 条的数据也远比 order by rand() 方法的组成临时表 R 字段排序再获取 rowid 代价小的多

模仿上面算法的思路,如果要随机取得三个值呢?

1.  取得整个表的行数,记为 C ;

2.  根据相同的随机方法得到 Y1 、 Y2 、 Y3 ;  (Y = floor(C * rand())

3.  再执行三个 limit Y, 1 语句得到三行数据。

select count(*) into @C from t;
set @Y1 = floor(@C * rand());
set @Y2 = floor(@C * rand());
set @Y3 = floor(@C * rand());
select * from t limit @Y1 , 1 ; // 在应用代码里面取 Y1 、 Y2 、 Y3 值,拼出 SQL 后执行
select * from t limit @Y2 , 1 ;
select * from t limit @Y3 , 1 ;

三、group by的执行流程

执行下面的语句

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

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

                                       

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

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

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

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

3.  遍历完成后,再根据字段 m 做排序(group by默认会执行排序,得到结果集返回给客户端。

                                                           (8)group by和order by rand随机排序的优化_第3张图片

图中最后一步,对内存临时表的排序,我们之前介绍过:order by rand() 使用了内存临时表,内存临时表排序的时候使用了 rowid 排序方法(图中内存临时表中的R部分,对于有主键的 InnoDB 表来说,这个 rowid 就是主键 ID,对于没有主键的 InnoDB 表来说,这个 rowid 就是由系统生成的)。

其中,临时表的排序过程就是虚线框中的过程。

                                                          (8)group by和order by rand随机排序的优化_第4张图片

如果你的需求并不需要对结果进行排序,那你可以在 SQL 语句末尾增加 order by null(抑制groupby排序) ,也就是改成:

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

这样就跳过了最后排序的阶段,直接从临时表中取数据返回。

这个例子里由于临时表只有 10 行,内存可以放得下,因此全程只使用了内存临时表。但是,内存临时表的大小是有限制的,超过则会把把内存临时表转成磁盘临时表,磁盘临时表默认使用的引擎是 InnoDB 

四、group by  优化方法 -- 索引抑制排序

不论是使用内存临时表还是磁盘临时表, group by 逻辑都需要构造一个带唯一索引的表,执行代价都是比较高的

执行 group by 语句为什么需要临时表?group by 的语义逻辑,是统计不同的值出现的个数。但是,由于每一行的 id%100 的结果是无序
的,所以我们就需要有一个临时表,来记录并统计结果。

那么,如果扫描过程中可以保证出现的数据是有序的,输入的数据是有序的:

                                                          (8)group by和order by rand随机排序的优化_第5张图片

计算 group by 的时候,就只需要从左到右,顺序扫描,依次累加。也就是下面这个过程:

  • 当碰到第一个 1 的时候,已经知道累积了 X 个 0 ,结果集里的第一行就是 (0,X);
  • 当碰到第一个 2 的时候,已经知道累积了 Y 个 1 ,结果集里的第一行就是 (1,Y);

按照这个逻辑执行的话,扫描到整个输入的数据结束,就可以拿到 group by 的结果,不需要临时表,也不需要再额外排序。 InnoDB 的索引,就可以满足这个输入有序的条件

在 MySQL 5.7 版本支持了 generated column 机制,用来实现列数据的关联更新。你可以用下面的方法创建一个列 z ,然后在 z 列上创建一个索引(如果是 MySQL 5.6 及之前的版本,你也可以创建普通列和索引,来解决这个问题)。

alter table t1 add column z int generated always as(id % 100), add index(z);

索引 z 上的数据就是类似 有序的了。上面的 group by 语句就可以改成:这样就不再需要临时表,也不需要排序了

select z, count(*) as c from t1 group by z;

五、group by 优化方法 -- 直接排序

如果碰上不适合创建索引的场景,我们还是要老老实实做排序的:一个 group by 语句中需要放到临时表上的数据量特别大,却还是要按照 “ 先放到内存临时表,插入一部分数据后,发现内存临时表不够用了再转成磁盘临时表 ” ,

可以由方法让我们直接走磁盘临时表的方法

在 group by 语句中加入 SQL_BIG_RESULT 这个提示( hint ),就可以告诉优化器:这个语句涉及的数据量很大,请直接用磁盘临时表。磁盘临时表是 B+ 树存储,存储效率不如数组来得高,那从磁盘空间考虑,还是直接用数组来存吧

下面这个语句的执行流程是这样的:

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

1.  初始化 sort_buffer ,确定放入一个整型字段,记为 m
2.  扫描表 t1 的索引 a ,依次取出里面的 id 值 ,  将 id%100 的值存入 sort_buffer 中;
3.  扫描完成后,对 sort_buffer 的字段 m 做排序(如果 sort_buffer 内存不够用,就会利用磁盘临时文件辅助排序);

4.  排序完成后,就得到了一个有序数组。根据有序数组,得到数组里面的不同值,以及每个值的出现次数

                                                   (8)group by和order by rand随机排序的优化_第6张图片

从 Extra 字段可以看到,这个语句的执行没有再使用临时表,而是直接用了排序算法

MySQL 什么时候会使用内部临时表?

1.  如果语句执行过程可以一边读数据,一边直接得到结果,是不需要额外内存的,否则就需要额外的内存,来保存中间结果;
2. join_buffer 是无序数组, sort_buffer 是有序数组,临时表是二维表结构;
3.  如果执行逻辑需要用到二维表特性,就会优先考虑使用临时表。比如我们的例子中, union需要用到唯一索引约束, group by 还需要用到另外一个字段来存累积计数。

六、总结

1.  如果对 group by 语句的结果没有排序要求,要在语句后面加 order by null ;
2.  尽量让 group by 过程用上表的索引,确认方法是 explain 结果里没有 Using temporary  和 Usingfilesort ;即可以通过索引来保证有序
3.  如果 group by 需要统计的数据量不大,尽量只使用内存临时表;也可以通过适当调大tmp_table_size 参数,来避免用到磁盘临时表;
4.  如果数据量实在太大,使用 SQL_BIG_RESULT 这个提示,来告诉优化器直接使用排序算法得到 group by 的结果。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:((8)group by和order by rand随机排序的优化)