「阿里MySQL面试题」内部临时表

bashendan
发布于 2023-8-25 16:03
浏览
0收藏

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)

该语句的执行流程:

  1. 创建一个内存临时表,该临时表只有一个整型字段f,并且f是主键字段
  2. 执行第一个子查询,得到1000这个值,并存入临时表中
  3. 执行第二个子查询:拿到第一行id=1000,试图插入临时表中。但由于1000这个值已经存在于临时表了,违反了唯一性约束,所以插入失败,然后继续执行取到第二行id=999,插入临时表成功。
  4. 从临时表中按行取出数据,返回结果,并删除临时表,结果中包含两行数据分别是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执行流程:

「阿里MySQL面试题」内部临时表-鸿蒙开发者社区

  1. 创建内存临时表,表里有字段m、c,主键m
  2. 扫描t1的索引a,依次取出叶子节点上的id值,计算id%10的结果,记为x;如果临时表中没有主键为x的行,就插入一个记录(x,1)如果表中有主键为x的行,就将x这一行的c值加1
  3. 遍历完成后,根据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。

若执行

「阿里MySQL面试题」内部临时表-鸿蒙开发者社区

把内存临时表的大小限制为最大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;

执行流程:

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

根据有序数组,得到数组里不同值,以及每个值的出现次数。

  • 使用 SQL_BIG_RESULT的执行流程
  • SQL_BIG_RESULT的explain
    该语句没有使用临时表,而直接用排序算法。

所以

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

若语句执行过程可以一边读数据,一边直接得到结果,就无需额外内存,否则就需额外内存,保存中间结果;

  • join_buffer是无序数组
  • sort_buffer是有序数组
  • 临时表是二维表结构

若执行逻辑需要用到二维表特性,就会优先考虑使用临时表。比如我们的例子中:

  • union需要用到唯一索引约束
  • group by还需要用到另外一个字段来存累积计数

如果感觉小编写得不错,请素质三连:点赞+转发+关注。我会努力写出更好的作品分享给大家。更多JAVA进阶学习资料小编已打包好


文章转载自今日头条:java星星

分类
标签
已于2023-8-25 16:03:41修改
收藏
回复
举报
回复
    相关推荐