【阿里MySQL面试题】内部临时表
sort buffer、内存临时表和join buffer,都是用来存放语句执行过程中的中间数据,以辅助SQL语句的执行。在排序的时候用到了sort buffer,在使用join语句的时候用到了join buffer。
union 执行流程
-
示例表
-
创建数据
执行如下SQL:
id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | PRIMARY | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | No tables used |
2 | UNION | t1 | NULL | index | NULL | PRIMARY | 4 | NULL | 2 | 100 | Backward index scan; Using index |
NULL | UNION RESULT | <union1,2> | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary |
这条语句用到了union,它的语义是,取这两个子查询结果的并集。重复的行只保留一行。
- key=PRIMARY,说明第二个子句用索引id。
- Extra字段,表示在对子查询的结果集做union时,使用了临时表(Using temporary)
该语句的执行流程:
- 创建一个内存临时表,该临时表只有一个整型字段f,并且f是主键字段
- 执行第一个子查询,得到1000这个值,并存入临时表中
- 执行第二个子查询:
- 拿到第一行id=1000,试图插入临时表中。但由于1000这个值已经存在于临时表了,违反了唯一性约束,所以插入失败,然后继续执行
- 取到第二行id=999,插入临时表成功。
- 从临时表中按行取出数据,返回结果,并删除临时表,结果中包含两行数据分别是1000和999。
- union 执行流程
这里内存临时表用于暂存数据,而且计算过程还用上了临时表主键id的唯一性约束,实现了union语义。
若把上面语句的union改成union all,就失去了“去重”语义。执行时,就依次执行子查询,得到的结果直接作为结果集的一部分,发给客户端。也就不需要临时表了。
- union all的执行计划
Extra=Using index,只使用了覆盖索引,没有用临时表。
group by 执行流程
把t1里的数据,按照 id%10 进行分组统计,并按m的结果排序后输出。
- group by 的执行计划
在Extra字段里面,我们可以看到三个信息:
- Using index,使用覆盖索引,选择了索引a,不需回表
- Using temporary,使用临时表
- Using filesort,需要排序
group by执行流程:
- 创建内存临时表,表里有字段m、c,主键m
- 扫描t1的索引a,依次取出叶子节点上的id值,计算id%10的结果,记为x;
- 如果临时表中没有主键为x的行,就插入一个记录(x,1)
- 如果表中有主键为x的行,就将x这一行的c值加1
- 遍历完成后,根据m排序,得到结果集返回给客户端
图中最后一步,对内存临时表的排序
- 内存临时表排序流程
临时表的排序过程就是图中虚线框
如果你的需求并不需要对结果进行排序,那你可以在SQL语句末尾增加order by null,也就是改成:
select id%10 as m, count(*) as c from t1 group by m order by null;
这样就跳过了最后排序,直接从临时表取数据返回:
- group + order by null 的结果(内存临时表)
由于t1中的id值从1开始,因此返回的结果集中第一行是id=1;扫描到id=10的时候才插入m=0
由于临时表只有10行,内存可以放得下,因此全程只使用内存临时表。
内存临时表的大小是有限制的,参数tmp_table_size就是控制这个内存大小的,默认16M。
若执行
把内存临时表的大小限制为最大1024K,并把语句改成id % 100
,这样返回结果里有100行数据。但这时内存临时表大小存不下这100行。
此时会把内存临时表转成磁盘临时表,磁盘临时表默认使用的引擎是InnoDB。 这时,返回的结果如图:
- group + order by null 的结果(磁盘临时表)
若t1的数据量很大,可能该查询需要的磁盘临时表就会占用大量磁盘空间。
优化group by
索引
无论内存临时表还是磁盘临时表,group by都需要构造一个带唯一索引的表,执行代价较高。若表数据量较大,上面这个group by执行就很慢。
为何执行group by需要临时表?
group by是统计不同的值出现的个数。但由于每行的id%100
结果无序,所以需要有一个临时表,来记录并统计结果。
若扫描过程可保证出现的数据有序,是不是简单了?
假设,现在有一个类似如下这么一个数据结构,我们来看看group by可以怎么做。
- group by算法优化-有序输入
所以,若确保输入数据有序,则计算group by时,就只需从左到右,顺序扫描,依次累加: - 当碰到第一个1时,已经知道累积了X个0,结果集里的第一行就是(0,X)
- 当碰到第一个2的时候,已经知道累积了Y个1,结果集里的第二行就是(1,Y);
按照这个逻辑执行的话,扫描到整个输入的数据结束,即可拿到group by的结果,无需临时表,也无需额外排序。
InnoDB索引刚好满足这个输入有序。
MySQL 5.7支持generated column,以实现列数据的关联更新。
创建一个列z,然后在z创建索引(≤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 优化的执行计划
从 Extra 可知该语句不再需要临时表,也无需排序。
直接排序
若可以通过加索引完成group by自然很棒。但若碰上不适合创建索引的场景,还是要做排序。
此时group by怎么优化?
若我们明知道,一个group by需要放到临时表上的数据量很大,却还是要“先放到内存临时表,插入一部分数据后,发现内存临时表不够用了再转成磁盘临时表”,就很蠢了
那这MySQL有无直接走磁盘临时表的方法?
有的。
在group by加入SQL_BIG_RESULT这个提示(hint),就可以告诉优化器:这个语句涉及的数据量很大,请直接用磁盘临时表。
MySQL的优化器一看,磁盘临时表是B+树存储,存储效率不如数组。所以,既然你告诉我数据量很大,那从磁盘空间考虑,还是直接用数组存。
因此,下面这个语句
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内存不够用,就会利用磁盘临时文件辅助排序)
- 排序完成后,就得到了一个有序数组。
根据有序数组,得到数组里不同值,以及每个值的出现次数。
- 使用 SQL_BIG_RESULT的执行流程
- SQL_BIG_RESULT的explain
该语句没有使用临时表,而直接用排序算法。
所以
MySQL什么时候会使用内部临时表?
若语句执行过程可以一边读数据,一边直接得到结果,就无需额外内存,否则就需额外内存,保存中间结果;
- join_buffer是无序数组
- sort_buffer是有序数组
- 临时表是二维表结构
若执行逻辑需要用到二维表特性,就会优先考虑使用临时表。比如我们的例子中:
- union需要用到唯一索引约束
- group by还需要用到另外一个字段来存累积计数
- 点赞
- 收藏
- 关注作者
评论(0)