-
Notifications
You must be signed in to change notification settings - Fork 0
Description
示例表
create table t1(id int primary key, a int, b int, index(a));
delimiter ;;
create procedure idata()
begin
declare i int;
set i=1;
while(i<=1000)do
insert into t1 values(i, i, i);
set i=i+1;
end while;
end;;
delimiter ;
call idata();
union 的执行
(select 1000 as f) union (select id from t1 order by id desc limit 2);
下图为该语句的 explain 结果
可以看到:
- 第二行 key = PRIMARY,说明第二个子句用到了索引 id
- 第三行 Extra 字段,表示在对子查询的结果集做 union 的时候,使用了临时表(Using temporary)
这个语句的执行流程如下:
- 创建一个内存临时表,这个临时表只有一个整型字段 f,并且 f 是主键字段
- 执行第一个子查询,得到 1000 这个值,并存入临时表中
- 执行第二个子查询:
- 拿到第一行 id=1000,试图插入临时表中。但由于 1000 这个值已经存在于临时表了,违反了唯一性约束,所以插入失败,然后继续执行
- 取到第二行 id=999,插入临时表成功
- 从临时表中按行取出数据,返回结果,并删除临时表,结果中包含两行数据分别是 1000 和 999
流程图如下:
这里的内存临时表起到了暂存数据的作用,而且计算过程还用上了临时表主键 id 的唯一性约束,实现了 union 的语义
如果 union 改成 union all 的话,就没有了去重的语义,这样执行的时候也就用不到临时表了,查询的结果直接作为结果集的一部分了
group by 的执行
select id%10 as m, count(*) as c from t1 group by m;
下图为该语句的 explain 结果
可以看到:
- Using index,表示这个语句使用了覆盖索引,选择了索引 a,不需要回表
- Using temporary,表示使用了临时表
- Using filesort,表示需要排序
这个语句的执行流程如下:
- 创建内存临时表,表里有两个字段 m 和 c,主键是 m
- 扫描表 t1 的索引 a,依次取出叶子节点上的 id 值,计算 id % 10 的结果,记为 x
- 如果临时表中没有主键为 x 的行,就插入一个记录 (x,1)
- 如果表中有主键为 x 的行,就将 x 这一行的 c 值加 1
- 遍历完成后,再根据字段 m 做排序,得到结果集返回给客户端
流程图如下:
如果不需要排序,可以使用 order by null,直接从临时表取数据返回
内存临时表的大小是有限制的,由参数 tmp_table_size 控制,默认为 16 M,如果内存临时表大小到达了上限,这时候会把内存临时表转换成磁盘临时表,磁盘临时表默认使用 InnoDB
group by 优化方法----索引
group by 的语义逻辑是统计不同的值出现的个数,但是由于每一行 id % 100 的结果是无序的,所以就需要一个临时表,来记录并统计结果
如果可以保证出现的数据是有序的,就可以避免临时表的使用
执行过程:
- 当碰到第一个 1 的时候,已经知道累积了 X 个 0,结果集的第一行就是 (0,X)
- 当碰到第一个 2 的时候,已经知道累计了 Y 个 1,结果集的第二行就是 (1,Y)
按照这个逻辑,扫描到数据结束就可以直接拿到 group by 的结果,不再需要临时表
在 MySQL 5.7 版本支持了 generated column 机制,用来实现列数据的关联更新。如果是 MySQL 5.6 及之前的版本,可以创建普通列和索引来解决这个问题
alter table t1 add column z int generated always as(id % 100), add index(z);
group by 语句可以改为:
select z, count(*) as c from t1 group by z;
explain 的结果:
group by 优化方法----直接排序
可以在 group by 语句中加入 SQL_BIG_RESULT 这个提示(hint),告诉优化器这个语句涉及的数据量很大,直接用磁盘临时表,这样就省去了放入内存临时表的步骤
select SQL_BIG_RESULT id%100 as m, count(*) as c from t1 group by m;
执行流程:
- 初始化 sort_buffer,确定放入一个整型字段,记为 m
- 扫描表 t1 的索引 a,依次取出里面的 id 值,将 id % 100 的值存入 sort_buffer 中
- 扫描完成后,对 sort_buffer 的字段 m 做排序(如果 sort_buffer 内存不够用,就会利用到磁盘临时文件辅助排序)
- 排序完成后,就得到一个有序数组
- 执行 group by 得到结果集
执行流程图:
explain 结果:
总结
- 如果语句执行过程可以一边读数据,一边直接得到结果,是不需要额外内存的,否则就需要额外的内存,来保存中间结果
- join_buffer 是无序数据,sort_buffer 是有序数组,临时表是二维表结构
- 如果只选逻辑需要用到二维表特性,就会优先考虑使用临时表。例如 union 需要用到唯一索引约束,group by 需要用到另外一个字段来保存累计计数