MySQL 中的临时表

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

出现场景

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

特色

 

一、建表语法是 create temporary table …。数据库

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

三、临时表能够与普通表同名。session

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

五、show tables 命令不显示临时表。函数

种类

临时表分为磁盘临时表和内存临时表。磁盘临时表指的是存储在磁盘上的临时表,由于在磁盘上,因此执行效率比较低,优势结构能够是有序的,实现能够是 InnoDB(默认),MyISAM 引擎;内存临时表就是存储在内存中,执行效率高,经常使用的实现引擎是 Memory。性能

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

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

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

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

 

Memory 引擎

与 InnoDB 的区别

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

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

三、数据位置发生变化的时候,InnoDB 表只须要修改主键索引,而内存表须要修改全部索引

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

五、InnoDB 支持变长数据类型,不一样记录的长度可能不一样;内存表不支持 Blob 和 Text 字段,而且即便定义了 varchar(N),实际也看成 char(N),也就是固定长度字符串来存储,所以内存表的每行数据长度相同

六、内存表支持 hash 索引,而且数据存储在内存中,因此执行比数据存储在磁盘上的 Innodb 快

缺点

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

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

虽然 Memory 引擎看起来缺点不少,可是由于其存储在内存中,而且关机后会自动清除数据,因此其是做为临时表的一个绝佳选择。

 

 

常见的应用场景

分库分表查询

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

一、若是只使用分区键做为查询条件如 select v from ht where f=N,那么直接经过分表规则找到 N 所在的表,而后去该表上查询就能够了。

二、若是使用其余字段做为条件且须要排序如 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:

能够知道:

一、左边语句没有进行查表操做  二、右边语句使用了 id 索引  三、联合时使用了临时表

具体过程

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

 

排序返回的字段过大

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

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

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

过程

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

 

group by 做为中间表

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

首先解析 SQL:

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

执行过程

一、建立内存临时表,表里有两个字段 m 和 c,主键是 m;
二、扫描表 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;

t一、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亿次。

具体过程

一、把表 t1 的全部字段取出来,存入 join_buffer 中。这个表只有 1000 行,join_buffer_size 默认值是 256k,能够彻底存入。
二、扫描表 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);

这样执行过程就变成: 

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

 

为何临时表能够重名

能够看到在 sessionA 在已经建立了一个名为 t1 的临时表,而且 sessionA 未结束前,sessionB 也建立了一个名为 t1 的临时表,没有发生异常。这是为何?

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

一、对于磁盘表:

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

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

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

二、对于内存表:

  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 格式下直接记录的是通过临时表得出的具体要操做的数据。

 

总结

临时表是一种很是方便的结构么,由于其会随着会话结束而自动删除,因此在一些查询效率较低但筛选字段使用不多的场景,就能够经过建立临时表,而后在临时表上建立索引来提升查询效率,同时也避免了索引的后续维护,而在其余复杂操做中,临时表也能够充当中间表的做用。因此临时表普遍出如今查询(多表联查)、分组、排序(排序返回的字段总长度过大)等场景中。

总结:

一、若是语句执行过程能够一边读数据,一边直接获得结果,是不须要额外内存的,不然就须要额外的内存,来保存中间结果;
二、join_buffer 是无序数组,sort_buffer 是有序数组,内存临时表是二维表结构,无序;磁盘临时表默认是B+结构,能够是数组,有序。
三、若是执行逻辑须要用到二维表特性,就会优先考虑使用临时表。好比咱们的例子中,union 须要用到惟一索引约束, group by 还须要用到另一个字段来存累积计数。

相关文章
相关标签/搜索