在使用 explain 解析一个 sql 时,有时我们会发现在 extra 列上显示 using temporary ,这表示这条语句用到了临时表,那么临时表究竟是什么?它又会对 sql 的性能产生什么影响?又会在哪些场景中出现?本文根据 <<MySQL 实战 45 讲>> 学习整理。

出现场景

其实临时表在之前的博客就已经出现过了,在 MySQL 中的排序 一文中就说到如果 order by 的列上没有索引,或者说没有用到索引,那么就需要进行额外排序(using filesort),而额外排序优先在一块 sort_buffer 空间中进行,如果这块空间大小小于要加载的字段总长度,那么就会用到临时文件辅助排序,这个临时文件就是临时表。临时表的作用就是作为中间表优化操作,比如 group by 作为分组的中间表, order by rand() (MySQL 中的排序 中的例子)作为中间表帮助运算等。

特点

1、建表语法是 create temporary table …。

2、一个临时表只能被创建它的 session 访问,对其他线程不可见,在会话结束后自动删除。所以,图中 session A 创建的临时表 t,对于 session B 就是不可见的。(所以特别适合用于join 优化)

3、临时表可以与普通表同名。

4、session A 内有同名的临时表和普通表的时候,show create 语句,以及增删改查语句访问的是临时表。

5、show tables 命令不显示临时表。

种类

临时表分为磁盘临时表和内存临时表。磁盘临时表指的是存储在磁盘上的临时表,因为在磁盘上,所以执行效率比较低,优点结构可以是有序的,实现可以是 InnoDB(默认),MyISAM 引擎;内存临时表就是存储在内存中,执行效率高,常用的实现引擎是 Memory。

磁盘临时表和内存临时表的区别

1、相比于 InnoDB 表,使用内存表不需要写磁盘,往表 temp_t 的写数据的速度更快;

2、索引 b 使用 hash 索引,查找的速度比 B-Tree 索引快;

3、临时表数据只有 2000 行,占用的内存有限。

Memory 引擎

与 InnoDB 的区别

1、InnoDB 表的数据总是有序存放的,而内存表的数据就是按照写入顺序存放的;关于这点可以通过创建 b+ 索引来进行排序,优化查询。alter table t1 add index a_btree_index using btree (id);

2、当数据文件有空洞的时候,InnoDB 表在插入新数据的时候,为了保证数据有序性,只能在固定的位置写入新值,而内存表找到空位就可以插入新值;

3、数据位置发生变化的时候,InnoDB 表只需要修改主键索引,而内存表需要修改所有索引;

4、InnoDB 表用主键索引查询时需要走一次索引查找,用普通索引查询的时候,需要走两次索引查找。而内存表没有这个区别,所有索引的“地位”都是相同的。

5、InnoDB 支持变长数据类型,不同记录的长度可能不同;内存表不支持 Blob 和 Text 字段,并且即使定义了 varchar(N),实际也当作 char(N),也就是固定长度字符串来存储,因此内存表的每行数据长度相同。

6、内存表支持 hash 索引,并且数据存储在内存中,所以执行比数据存储在磁盘上的 Innodb 快。

缺点

1、锁粒度大,只支持表级锁,并发度低。

2、数据持久性差。因为是内存结构,所以在重启后数据会丢失 。由此会导致备库在硬件升级后数据就会丢失,并且如果主从库互为 "主备关系" ,备库在关闭后还会将删除数据记录进 binlog,重启后主机会执行备库发送过来的 binlog ,导致主库数据也会丢失。

虽然 Memory 引擎看起来缺点很多,但是因为其存储在内存中,并且关机后会自动清除数据,所以其是作为临时表的一个绝佳选择。

常见的应用场景

分库分表查询

将一个大表 ht,按照字段 f,拆分成 1024 个分表,然后分布到 32 个数据库实例上(水平分表)。一般情况下,这种分库分表系统都有一个中间层 proxy。不过,也有一些方案会让客户端直接连接数据库,也就是没有 proxy 这一层。假设分区键是 列 f 。

1、如果只使用分区键作为查询条件如 select v from ht where f=N,那么直接通过分表规则找到 N 所在的表,然后去该表上查询就可以了。

2、如果使用其他字段作为条件且需要排序如 select v from ht where k >= M order by t_modified desc limit 100,那么非但不能确定要查询的记录在哪张表上,而且因为默认使用的是分区键排序,所以得到的结果还是无序的,需要额外排序。

  1)在 proxy 层完成排序。优势是速度快,缺点是开发工作量比较大,如果涉及复杂的操作如 group by,甚至 join 这样的操作,对中间层的开发能力要求比较高。并且还容易出现内存不够、CPU 瓶颈的问题。

  2)将各个分区的查询结果(未排序)总结到一张临时表上进行排序。 

    Ⅰ、在汇总库上创建一个临时表 temp_ht,表里包含三个字段 v、k、t_modified;
    Ⅱ、在各个分库上执行 select v,k,t_modified from ht_x where k >= M order by t_modified desc limit 100;
    Ⅲ、把分库执行的结果插入到 temp_ht 表中;
    Ⅳ、执行 select v from temp_ht order by t_modified desc limit 100;

union 作为中间表

有表t1: create table t1(id int primary key, a int, b int, index(a)); 有记录(1,1,1) 到 (1000,1000,1000)      执行 (select 1000 as f) union (select id from t1 order by id desc limit 2);

解析这条 sql:

可以知道:

1、左边语句没有进行查表操作  2、右边语句使用了 id 索引  3、联合时使用了临时表

具体过程:

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

排序返回的字段过大

举一个在  MySQL中的排序 中提到过的例子。

select word from words order by rand() limit 3;    表数据有10000行      SQL是从10000行记录中随机获取3条记录返回。

这个执行过程因为涉及到 rand() 且数据量比较大,所以单靠 sort_buffer 排序空间不够,所以还用到临时表。

过程

1、从缓冲池依次读取记录,每次读取后都调用 rand() 函数生成一个 0-1 的数存入内存临时表,W 是 word 值,R 是 rand() 生成的随机数。到这扫描了 10000 行。
2、初始化 sort_buffer,从内存临时表中将 rowid(这张表自动生成的) 以及 排序数据 R 存入 sort_buffer。到这因为要遍历内存临时表所以又扫描了 10000 行。
3、在 sort_buffer 中根据 R 排好序,然后选择前三个记录的 rowid 逐条去内存临时表中查到 word 值返回。到这因为取了三个数据去内存临时表去查找所以又扫描了 3 行。总共 20003 行。

group by 作为中间表

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

首先解析 SQL:

可以看到使用了临时表和额外排序,接下来来解析

执行过程:

1、创建内存临时表,表里有两个字段 m 和 c,主键是 m;
2、扫描表 t1 的索引 a,依次取出叶子节点上的 id 值,计算 id%10 的结果,记为 x;
  1)如果临时表中没有主键为 x 的行,就插入一个记录 (x,1);
  2)如果表中有主键为 x 的行,就将 x 这一行的 c 值加 1;
遍历完成后,再根据字段 m 做排序,得到结果集返回给客户端。

排序的过程就按照排序规则进行,用到 sort_buffer ,可能用到临时表。

优化 BNL 排序

表结构:

CREATE TABLE `t2` (
`id` int(11) NOT NULL,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `a` (`a`)
) ENGINE=InnoDB;

t1、t2 结构相等,t2 100万条数据,t1 1000行数据,t1 的数据在 t2 上都有对应,相等。执行语句:select * from t1 join t2 on (t1.b=t2.b) where t2.b>=1 and t2.b<=2000;

分析:因为字段b 没有创建索引,所以排序是属于 BNL 排序,再加上数据量比较大,所以在比较时扫描的总行数就等于  100万*1000,也就是10亿次。

具体过程

1、把表 t1 的所有字段取出来,存入 join_buffer 中。这个表只有 1000 行,join_buffer_size 默认值是 256k,可以完全存入。
2、扫描表 t2,取出每一行数据跟 join_buffer 中的数据进行对比,
  1)如果不满足 t1.b=t2.b,则跳过;
  2)如果满足 t1.b=t2.b, 再判断其他条件,也就是是否满足 t2.b 处于[1,2000]的条件,如果是,就作为结果集的一部分返回,否则跳过。

优化

如果筛选字段用的比较多,那么可以为其创建索引,使 BNL 优化成 NLJ,但是如果这个字段使用的不多,那么为其创建索引反倒会因为多了不必要的维护成本而降低总体的性能。所以。针对于使用率不高的 BNL 筛选字段的优化,可以创建一个临时表,让这个临时表作为一个索引表,来优化成 NLJ,同时因为临时表在会话结束后会自动删除,省去了维护成本。

create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb;
insert into temp_t select * from t2 where b>=1 and b<=2000;
select * from t1 join temp_t on (t1.b=temp_t.b);

这样执行过程就变成:

1、执行 insert 语句构造 temp_t 表并插入数据的过程中,对表 t2 做了全表扫描,这里扫描行数是 100 万。
2、之后的 join 语句,扫描表 t1,这里的扫描行数是 1000;join 比较过程中,做了 1000 次带索引的查询(因为t1 1000行,作为驱动表,t2作为被驱动表)。相比于优化前的 join 语句需要做 10 亿次条件判断来说,这个优化效果还是很明显的。

为什么临时表可以重名

可以看到在 sessionA 在已经创建了一个名为 t1 的临时表,并且 sessionA 未结束前,sessionB 也创建了一个名为 t1 的临时表,没有发生异常。这是为什么?

首先要知道在 MySQL 启动后每张表都会加载到内存中,所以每张表都分为内存表和磁盘表。

1、对于磁盘表:

  1)普通表的表结构和数据文件都是存储在库名文件夹下的,文件名就是表名。

  2)结构文件存储在临时文件夹下,文件的后缀是 frm,前缀是 "#sql{进程 id}_{线程id}_序列号";

     数据文件在 5.6 及之前是存储在临时文件夹下的,5.7 开始存放在专门存放临时文件数据的临时表空间。

2、对于内存表:

  1)普通表的命名是 "库名 + 表名"。

  2)临时表的命名则在 " 库名 + 表名 " 的基础上,加入了 " server_id + thread_id "。比如:

  session A 的临时表 t1,在备库的 table_def_key 就是:库名 +t1+“M 的 serverid”+“session A 的 thread_id”;
  session B 的临时表 t1,在备库的 table_def_key 就是 :库名 +t1+“M 的 serverid”+“session B 的 thread_id”。

综上所述,因为临时表在磁盘和内存中表的命名都取自具体的进程id、线程id、所以可以实现不同的会话创建相同的表名。

如果 binlog 的格式是 row,那么是不会记录临时表的各个操作的,因为临时表就是用于辅助各自操作的,所以在 row 格式下直接记录的是经过临时表得出的具体要操作的数据。

总结

临时表是一种非常方便的结构么,因为其会随着会话结束而自动删除,所以在一些查询效率较低但筛选字段使用很少的场景,就可以通过创建临时表,然后在临时表上创建索引来提高查询效率,同时也避免了索引的后续维护,而在其他复杂操作中,临时表也可以充当中间表的作用。所以临时表广泛出现在查询(多表联查)、分组、排序(排序返回的字段总长度过大)等场景中。

总结:

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

MySQL 中的临时表的更多相关文章

  1. MySQL中的临时表到底什么是?

    Author:极客小俊 一个专注于web技术的80后 我不用拼过聪明人,我只需要拼过那些懒人 我就一定会超越大部分人! CSDN@极客小俊,原创文章, B站技术分享 B站视频 : Bilibili.c ...

  2. MySQL中进行树状所有子节点的查询

    在Oracle 中我们知道有一个 Hierarchical Queries 通过CONNECT BY 我们可以方便的查了所有当前节点下的所有子节点.但很遗憾,在MySQL的目前版本中还没有对应的功能. ...

  3. MySQL中进行树状所有子节点的查询 . mysql根据父id 查询所有的子id

    在Oracle 中我们知道有一个 Hierarchical Queries 通过CONNECT BY 我们可以方便的查了所有当前节点下的所有子节点.但很遗憾,在MySQL的目前版本中还没有对应的功能. ...

  4. mysql中递归树状结构<转>

    在Oracle 中我们知道有一个 Hierarchical Queries 通过CONNECT BY 我们可以方便的查了所有当前节点下的所有子节点.但很遗憾,在MySQL的目前版本中还没有对应的功能. ...

  5. MySQL中的两种临时表

    MySQL中的两种临时表 伯乐在线2016-07-06 05:16:52阅读(4556)评论(3) 声明:本文由入驻搜狐公众平台的作者撰写,除搜狐官方账号外,观点仅代表作者本人,不代表搜狐立场.举报 ...

  6. MySQL 中的两种临时表

    来源:阿里云RDS - 数据库内核组 链接:http://mysql.taobao.org/monthly/2016/06/07/ 外部临时表 通过CREATE TEMPORARY TABLE 创建的 ...

  7. mysql中bit_count和bit_or函数的含义

    翻阅mysql手册时,看到有个示例使用了bit_or方法来去除重复的数据,一开始没看明白,后来看明白之后感觉非常巧妙.示例要实现的功能就是计算每月有几天有访问,先把示例摘录在这里. 1 2 3 4 5 ...

  8. MySQL中如何查看“慢查询”,如何分析执行SQL的效率?

    一.MySQL数据库有几个配置选项可以帮助我们及时捕获低效SQL语句 1,slow_query_log这个参数设置为ON,可以捕获执行时间超过一定数值的SQL语句. 2,long_query_time ...

  9. [转]MySQL中存储过程权限问题

    MySQL中以用户执行存储过程的权限为EXECUTE 比如我们在名为configdb的数据库下创建了如下存储过程,存储过程的定义者为user_admin use configdb; drop proc ...

随机推荐

  1. 【剑指offer】04 重建二叉树

    题目地址:重建二叉树 题目描述                                    输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树.假设输入的前序遍历和中序遍历的结果中都不 ...

  2. Mysql实现定时清空一张表的旧数据并保留几条数据

    要达到如下目的: Mysql数据库会每隔一段时间(可以是2小时,也可以是一天,这个可以自定义),定时对一张库中的表做一个判断,如果这张表的数据超过了20条(这个数据也是自定义的,也可以是200条),就 ...

  3. sqlserver 汉字转全拼函数

    create function fn_Getquanpin (@str varchar(100)) returns varchar(8000) as begin declare @re varchar ...

  4. Abp(net core)+easyui+efcore实现仓储管理系统——出库管理之八(五十七)

    abp(net core)+easyui+efcore实现仓储管理系统目录 abp(net core)+easyui+efcore实现仓储管理系统--ABP总体介绍(一) abp(net core)+ ...

  5. Spring boot 启动错误处理:Action: Consider the following: If you want an embedded database (H2, HSQL or Derby), please put it on the classpath. If you have database settings to be loaded from a particular...

    错误原因 在pom中引入了mybatis-spring-boot-starter ,Spring boot默认会加载org.springframework.boot.autoconfigure.jdb ...

  6. break continue学习

    1.break和continue必须使用在循环或者switch语句中 2.当break语句单独存在时.后面不要定义其他语句,因为后面的语句无法执行到 3.break跳出当前所在循环 4.break跳出 ...

  7. 本地项目上传至GitHub

    本地项目上传至GitHub 使用git上传 一.安装git 直接官网下载,安装即可. git官网下载 github下载 按照好后大概就是这个样子 二.创建公钥和私钥 有的就可跳过此步骤 我们双击打开g ...

  8. Java基础经典案例

    案例列表 01减肥计划switch版本 02减肥计划if版本 03逢七跳过 04不死神兔 05百钱白鸡 06数组元素求和 07判断两个数组是否相同 08查找元素在数组中的索引 09数组元素反转 10评 ...

  9. ConcurrentHashMap 并发之美

    一.前言 她如暴风雨中的一叶扁舟,在高并发的大风大浪下疾驰而过,眼看就要被湮灭,却又在绝境中绝处逢生 编写一套即稳定.高效.且支持并发的代码,不说难如登天,却也绝非易事. 一直有小伙伴向我咨询关于Co ...

  10. 上班如何优雅的使用idea刷LeetCode(力扣)

    打开idea file->setting ->plugins 搜索 "LeetCode" install "LeetCode editor" 重启后 ...