
搞MySQL性能分析这些年临时表是我见过最容易被低估也最容易踩坑的一个功能。很多人觉得临时表就是“用一句CREATE TEMPORARY TABLE建一张会话级的表而已”但实际在复杂报表、数据清洗、多步骤批处理场景里临时表用得好能把一条跑几十秒的SQL优化到秒级用不好能把磁盘IO打满让整个数据库实例都跟着遭殃。这篇文章就围绕MySQL临时表把原理、实操、参数和排查经验一次讲透适合正在做SQL优化、需要处理复杂查询或者被“Using temporary”折磨过的人。我最早接触临时表是因为某报表平台的一条统计SQL。当时线上单表不到两千万行一条带三个JOIN加GROUP BY的报表查询跑了两百多秒经常把连接数占满。后来把它拆成临时表分步处理查询时间直接降到三秒以内。也是从那以后我开始认真研究临时表到底在MySQL里是怎么工作的。这篇文章不堆概念只讲我实际验证过的东西尽量说人话方便你直接拿去用。1. 临时表到底能解决什么问题临时表的核心定位就是一张“会话级草稿纸”。你在一个数据库连接里创建它往里面写数据做各种加工处理连接一断开表就自动消失不需要额外清理。这个特性天然适合处理那些“中间结果很重”的查询场景。1.1 为什么一个“会话级草稿箱”这么重要复杂SQL最怕的不是表大而是中间结果集被反复扫描。举个例子你要统计某天的订单金额还要关联用户维度和商品维度。如果全塞进一条SQLMySQL的优化器可能会先生成一个很大的JOIN结果集再对这个结果集做GROUP BY。这个结果集如果很大就会在内存或磁盘上反复读写性能自然上不去。这时候临时表的价值就出来了你可以先用一条较轻的SQL把订单汇总结果写入临时表再对临时表建索引最后和其他维度表做关联。因为临时表的数据量被提前压缩了后续JOIN和聚合的成本都会低很多。更重要的一点是临时表只在当前会话可见其他连接完全感知不到。这就相当于你自己开了一个小工作台上面摆满半成品干完活一走了之不留任何垃圾。实际工作中临时表特别适合下面几类场景一条SQL里有多层嵌套子查询各层结果被重复引用。同一个中间结果需要被后续多条SQL使用。做数据清洗时要对一批数据反复UPDATE和DELETE。批量更新前需要先圈定受影响的数据范围做核对。这些都是“中间状态”操作放在临时表里远比放在普通表里合适因为你不想因为一次调试失败就在业务库里留下一堆没人管的临时残留表。1.2 普通表和临时表的本质区别很多人分不清临时表和普通表的边界其实核心差异就在几个维度上。我把最关键的差别整理成了下面这张表对比项普通表临时表作用范围所有连接可见仅当前连接可见生命周期删除前一直存在连接关闭即自动销毁是否入业务库是影响存储和备份否不参与日常业务备份同名冲突不允许同库同名可与普通表同名会掩盖普通表写入binlog正常记录一般不建议依赖适合场景持久化业务数据计算过程中的临时中间态这里有一个非常容易踩的坑临时表可以和普通表同名而且在当前会话里你用这个表名写任何SQL访问到的都只会是临时表不是普通表。比如业务库有一张user表你又创建了一个名为user的临时表接下来在这个连接里执行SELECT * FROM user拿到的就是临时表数据。一旦你在临时表里做删除操作删除的也只是临时表内容普通表不受影响。这个特性听起来挺安全但实际很容易让人误判数据来源所以我的建议是除非你有非常明确的理由否则别让临时表和业务表同名。还有一个本质区别是存储位置。临时表的数据可以完全留在内存也可以落到磁盘具体取决于表的数据量、字段类型和MySQL参数设置。这一点下一章详细展开因为它直接决定临时表是“提速神器”还是“磁盘杀手”。2. 临时表的数据到底存到了哪里临时表听起来轻量但它的数据不一定都在内存里。MySQL对临时表的管理有一套比较复杂的机制搞清楚数据存放位置才能知道为什么有时候临时表很快有时候又会让整个实例变慢。2.1 先分清外部临时表和内部临时表我们平时用CREATE TEMPORARY TABLE创建的表在官方文档里通常叫“外部临时表”因为它是用户主动创建的可以手动控制。MySQL内部还有一类“内部临时表”是优化器在执行GROUP BY、DISTINCT、ORDER BY或者某些子查询合并操作时自动生成的你没法直接操作它只能通过EXPLAIN看到“Using temporary”这样的提示。区分这两点非常重要。如果你看到SHOW PROCESSLIST里有大量磁盘临时表产生不一定是你自己创建的表更可能是某条SQL触发了内部临时表。比如一个GROUP BY字段上没有索引的大查询优化器会把分组结果先放进一张内部临时表再排序返回。这张表如果超过内存阈值就会被挪到磁盘上大量的随机读写就会瞬间拖垮磁盘IO。内部临时表通常是为了完成排序或去重它本身没有业务含义优化器用完就扔。正因为如此你没法直接给它加索引也没法干预它的内容。想要优化内部临时表只能从SQL写法、索引设计上下功夫让优化器尽量少生成这种中间结构。而外部临时表随时可以加索引、加主键这是二者在处理方式上最大的不同。2.2 内存临时表的两个关键水位参数MySQL决定一张临时表是留在内存还是落盘主要看两参数tmp_table_size和max_heap_table_size。当一个外部临时表使用MEMORY存储引擎时它的大小超过这两个参数中的较小值时MySQL就会自动把表转到磁盘存储。实际上对于普通的MEMORY引擎临时表内存上限就是min(tmp_table_size, max_heap_table_size)。默认情况下两个参数都是16MB左右意味着你的临时表只要超过16MB就可能落盘。很多业务查询的中间结果动辄几百MB这时候如果还指望临时表全内存运行基本不现实。这里还要提一下MySQL 8.0里的变化。8.0引入了一个新的内部临时表存储引擎TempTable它的内存上限由temptable_max_ram参数控制默认是1GB超出后会把部分数据映射到磁盘文件。这个机制比早期版本更平滑不再是一到阈值就直接全量落盘而是内存和磁盘混合使用。所以在8.0环境下临时表处理大数据量时性能和稳定性都有明显提升。我的实际感受是调tmp_table_size和max_heap_table_size要谨慎。如果你把这两个值调到几百MB内存临时表确实能多留一会儿但要知道内存临时表占用的是buffer pool之外的内存调太大反而会增加内存压力。线上环境一般从64MB开始试观察Created_tmp_disk_tables是否下降不要一上来就搞到1GB。2.3 磁盘临时表的存储引擎选择临时表一旦落盘就涉及存储引擎选型。MySQL默认的外部临时表引擎由default_tmp_storage_engine控制可以选择MEMORY或InnoDB等。内部临时表落盘时的引擎则由internal_tmp_disk_storage_engine控制常见的选项是MyISAM和InnoDB。在不需要事务、数据量可控的场景里MEMORY引擎的临时表速度最快因为数据都在内存。但MEMORY引擎有几个限制字段类型不支持BLOB/TEXT表大小受max_heap_table_size限制而且VARCHAR之类变长字段的行存储会比较“吃内存”。如果你建临时表时发现报错“The BLOB/TEXT column xxx cant be used as key”或者行大小超限基本就是MEMORY引擎的目录限制。InnoDB临时表则没有这些字段限制崩溃恢复也更安全但写入性能比MEMORY引擎低一些。MySQL 8.0在默认配置下已经对临时表和TEMP目录做了更细的管理临时表可以使用InnoDB并享受独立的临时表空间。如果我的临时表里既有文本字段又要做关联查询我一般会选InnoDB引擎如果只是简单ID集合可以考虑MEMORY引擎但前提是你确定数据量不会明显超过内存阈值。3. 三个可以直接复用的临时表实操场景光讲原理不够我把平时用得最多的三个临时表场景写成完整步骤SQL可以直接抄但建议先理解为什么这么做。3.1 场景一复杂统计报表的中间结果集缓存某零售业务需要统计今天每个用户的下单金额和订单数然后关联用户等级表算出不同等级用户贡献的GMV。最初一条SQL大概长这样跑得很慢。我当时没有急着去优化JOIN顺序而是先看执行计划发现最终GROUP BY是在多表JOIN之后做的中间结果集特别大排序开销高。改进方案是先按用户维度做一次轻量聚合把结果物化到临时表里再跟等级表关联。示例代码如下CREATE TEMPORARY TABLE tmp_daily_user_summary ( user_id INT NOT NULL, total_amount DECIMAL(12,2) NOT NULL, order_cnt INT NOT NULL, PRIMARY KEY (user_id) ) ENGINEInnoDB; INSERT INTO tmp_daily_user_summary (user_id, total_amount, order_cnt) SELECT o.user_id, SUM(o.total_amount), COUNT(*) FROM orders o WHERE o.order_date CURRENT_DATE GROUP BY o.user_id;这还没结束关键一步是让临时表带主键。因为后续要和用户表关联主键索引能让关联查询直接走索引查找而不是对临时表做全表扫描。SELECT u.user_level, SUM(t.total_amount) AS gmv, SUM(t.order_cnt) AS order_count FROM user_level u INNER JOIN tmp_daily_user_summary t ON u.user_id t.user_id GROUP BY u.user_level;这样改写之后GROUP BY从原始大表提前到了临时表生成阶段临时表里只有当天有订单的用户量级比JOIN后的结果小很多。你可能会说用CTE公共表表达式也能实现类似效果但CTE在当前MySQL版本里默认不一定物化多次引用还可能被重复执行不如临时表可控。而且临时表能自己加索引CTE没法直接加这是临时表在复杂报表场景里的明显优势。3.2 场景二同一批数据的多次加工数据清洗和ETL里经常要拿到一批原始数据然后分阶段处理。比如导入了一批用户注册信息要先过滤掉非法手机号再识别重复身份证号最后按渠道统计有效用户数。如果每步都直接操作源表要么污染原始数据要么写一堆嵌套子查询。临时表非常适合这种“先收拢再逐步加工”的过程。我的写法是这样的CREATE TEMPORARY TABLE tmp_clean_user ( user_id INT NOT NULL, phone VARCHAR(20), id_card VARCHAR(30), channel VARCHAR(50), is_valid TINYINT DEFAULT 1 ) ENGINEInnoDB; INSERT INTO tmp_clean_user (user_id, phone, id_card, channel) SELECT user_id, phone, id_card, channel FROM register_log WHERE create_date CURRENT_DATE; UPDATE tmp_clean_user SET is_valid 0 WHERE phone IS NULL OR phone NOT REGEXP ^1[0-9]{10}$; UPDATE tmp_clean_user SET is_valid 0 WHERE id_card IN ( SELECT id_card FROM tmp_clean_user GROUP BY id_card HAVING COUNT(*) 1 ); SELECT channel, COUNT(*) AS total_cnt, SUM(is_valid) AS valid_cnt FROM tmp_clean_user GROUP BY channel;这段逻辑里有多次UPDATE和子查询如果直接在业务表上操作每执行一步都要担心删错数据。临时表就不一样所有数据都是我Copy进来的副本怎么改都不影响原始表。处理完一看不对DROP掉重来就行。而且同一个临时表可以被后续多条SQL反复引用不需要每次重跑原始数据拉取逻辑。这里有个小提醒如果你要UPDATE临时表的同时又去查这张表MySQL对同一张表的读写存在限制比较容易报出类似“You cant specify target table for update in FROM clause”的错误。上面第二轮UPDATE我用子查询去查同一张临时表就属于这类场景。我的处理办法是先套一层派生表把需要去重的ID集查出来再关联更新。或者更简单一点分两步先查重复ID再UPDATE避免同一条语句里读写同一张表。3.3 场景三批量更新前的数据校验批量更新永远有风险尤其是那种“我要把所有商品价格上调10%”的操作。条件一旦写错整张表都要被误更新。用临时表先把待更新的ID圈出来再执行UPDATE相当于给批量操作加了一道保险。比如某运营需求库存低于10且分类ID为12的商品价格上调10%。我不会直接去UPDATE products表而是先建一张临时ID表把受影响的主键全部放进去。CREATE TEMPORARY TABLE tmp_to_update ( product_id INT PRIMARY KEY ) ENGINEInnoDB; INSERT INTO tmp_to_update (product_id) SELECT product_id FROM products WHERE category_id 12 AND stock 10; SELECT COUNT(*) AS affected_rows FROM tmp_to_update;执行完COUNT之后我就能确认这次操作影响多少行。如果发现影响行数异常直接终止后续操作回到业务层核查条件。确认没问题再执行UPDATE products p INNER JOIN tmp_to_update t ON p.product_id t.product_id SET p.price p.price * 1.1;这个写法的优点很明显第一圈定数据的动作与更新动作分离谁先谁后看得很清楚第二临时表里只放主键关联更新时走的是PRIMARY KEY索引比WHERE条件里直接写一堆范围条件更可控第三如果更新过程中事务回滚临时表还在你可以手动核查不需要重新跑一遍原查询。这也是批量数据变更前非常实用的“预执行”手法。4. 临时表常见问题与排查实录临时表用多了遇到的问题也五花八门。这里把我实际踩过的坑和排查思路整理出来每条都是能在控制台或状态变量里直接看到的。4.1 磁盘临时表比例突然升高先看一组状态变量它们能直接反映临时表的创建和落盘情况SHOW GLOBAL STATUS LIKE Created_tmp%;一般会得到类似这样的值Created_tmp_disk_tables 168 Created_tmp_tables 1450Created_tmp_tables代表累计创建了多少张临时表Created_tmp_disk_tables代表有多少张落到了磁盘。如果磁盘临时表数量占临时表总数的比例超过20%或者短时间delta增长很快就说明系统里存在大量大型临时表操作。此时最常见的责任人有两个一是用户创建的临时表数据量超过了内存阈值二是SQL执行计划里的内部临时表落盘。排查思路是先看慢查询日志找到那些执行计划里带“Using temporary”的SQL再看看这些SQL是否涉及GROUP BY、DISTINCT或ORDER BY。如果少了索引优先通过加索引消除排序和分组产生的内部临时表。如果是用户创建的临时表落盘检查一下建表语句是不是用了MEMORY引擎以及字段大小是否过大。很多时候临时表落盘是因为字段定义太宽比如明明只存一个ID却定义了VARCHAR(500)行数不多也能撑爆内存阈值。我的习惯是把这个比例纳入日常巡检。如果某天突然升高往往不是参数问题而是有人上线了一条新SQL。这时候先定位SQL比先调参数更靠谱。4.2 连接池复用导致临时表“幽灵残留”临时表本来是连接断开自动消失但很多应用框架用了数据库连接池连接并没有真正断开而是归还到池里继续复用。这就会带来一个隐藏问题你在一个业务方法里创建了临时表方法结束没DROP连接被连接池回收。下一次这个方法或其他方法从连接池拿到同一个连接时那张临时表还在。结果就是再次创建同名临时表时报错“Table tmp_xxx already exists”或者更隐蔽的是你在一个方法里建了临时表另一个方法读到了旧数据排查半天都想不到是连接复用惹的祸。我见过最极端的情况是临时表跨了两个请求导致线上数据串了。解决办法只有两个一是所有临时表用完必须显式DROP哪怕连接关闭也会自动清理也不能赌这一步二是在连接归还给连接池前把当前会话内的临时表清掉。实际写代码时尽量用try/finally结构CREATE TEMPORARY TABLE tmp_user ...; ... DROP TEMPORARY TABLE IF EXISTS tmp_user;如果你是直接在数据库客户端里调试手动执行完临时表语句后也要记得DROP。别觉得反正断开连接就没了很多公司的数据库连接长时间不断你不删它就一直在。4.3 EXPLAIN中出现Using temporary怎么办这是临时表最常被提起的“罪证”。我先说结论Using temporary不一定代表性能差但代表优化器为这条SQL生成了一张内部临时表需要额外占用内存或磁盘。如果出现又叠加“Using filesort”基本就可以判断这条SQL存在排序和分组压力。遇到这种情况第一步是看临时表产生的原因。常见有这几种GROUP BY字段上没有索引分组操作无法走索引只能建临时表。ORDER BY字段和GROUP BY字段不一致需要额外的排序临时表。DISTINCT字段组合较大无法通过索引去重。查询的SELECT字段里有无法直接分组的大字段比如BLOB/TEXT。根据原因做针对性优化。最常见的是给GROUP BY和ORDER BY涉及的字段加联合索引。比如原来的SQL是按status分组、按create_time排序那么建立一个(status, create_time)的联合索引通常能同时消除文件排序和内部临时表。如果已经加了索引还是出现Using temporary可以考虑把SQL拆成两步用外部临时表先物化再关联。另外一些框架自动生成的很长的ORM查询动辄十几个字段也会导致临时表很大。这种情况下把查询缩小到需要的字段往往比调整数据库参数更有效。我自己有一条原则一条SQL里如果出现两个以上Using temporary基本就意味着这条SQL已经超出了合理的复杂度该拆就拆不要硬扛。4.4 复制架构下临时表需要注意什么在主从复制架构里临时表是最容易被忽略的风险点。因为临时表是会话级的在主库上创建、使用、删除都在同一会话里逻辑上没问题。但MySQL的binlog记录方式不同处理结果也会不同。在基于行级的复制模式下DML操作记录的是变更后的行数据临时表本身通常不会导致从库数据不一致。但在基于语句级的复制模式下主库上执行的CREATE TEMPORARY TABLE语句会记录到binlog从库在执行时可能因为没有这个临时表上下文而出错或者在回放时产生额外开销。更麻烦的是如果在存储过程或触发器中依赖临时表某些语句模式下临时表的内容变化可能造成主从执行结果不一致。所以我的建议是生产环境的复制架构尽量使用基于行级的binlog格式如果团队里有人写存储过程操作临时表要特别关注主从数据一致性。临时表本身不是问题但你不能把临时表当成跨会话传递数据的通道更不能假设从库也有这张临时表。主从切换、故障恢复时临时表不会跟着恢复这点要提前和业务方说清楚。5. 临时表相关的参数与SQL写法备忘最后把临时表相关的关键SQL写法和参数整理成一份可以直接查的“手卡”。这些内容不一定每天用到但用到的时候能帮你少踩很多坑。5.1 创建、索引、删除的正确姿势创建临时表时建议显式指定存储引擎尽量避免依赖当前会话的默认引擎设置。下面的写法最常用CREATE TEMPORARY TABLE tmp_user ( id INT NOT NULL PRIMARY KEY, name VARCHAR(50) NOT NULL, age INT NOT NULL, INDEX idx_age (age) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;如果你的临时表还需要在后续查询中高频过滤某个字段比如按age做关联可以追加索引ALTER TABLE tmp_user ADD INDEX idx_age (age);删除时一定写全TEMPORARY关键字DROP TEMPORARY TABLE IF EXISTS tmp_user;为什么强调这个如果你漏了TEMPORARY并且临时表和普通表同名DROP TABLE会直接删除普通业务表。这个风险非常致命已经有不少生产事故是因为DROP时没写TEMPORARY结果把线上表删了。所以不管什么情况下我都会在删除临时表时明确写TEMPORARY并且加上IF EXISTS防止重复删除报错。还有一个细节临时表可以和普通表同名但一旦同名当前会话里所有引用这个表名的SQL都指向临时表指向普通表的SQL调用会全部失效。如果你的ORM框架代码里有基于表名的操作建临时表时最好用tp_、tmp_这样的前缀避免和业务表重名。5.2 关键参数速查表下面这张表是我在实际调优中常看的一组参数不同版本默认值可能有差异大家以自己环境的实际输出为准。参数名默认值常见范围作用与建议tmp_table_size16MB外部MEMORY临时表内存上限之一建议从64MB起步观察max_heap_table_size16MB控制单张MEMORY表最大内存和tmp_table_size取较小值temptable_max_ram1GBMySQL 8.0内部TempTable引擎内存上限超出后逐步落盘internal_tmp_disk_storage_engineMyISAM/InnoDB内部临时表落盘引擎建议使用InnoDB获得崩溃恢复能力default_tmp_storage_engineMEMORY/InnoDB创建外部临时表时的默认引擎按场景显式指定更安全修改这些参数时要注意max_heap_table_size如果改太大MEMORY临时表会占用大量内存容易引发OOM。tmp_table_size只是阈值不是预分配的固定内存不用太担心空跑关键是别让临时表频繁在内存和磁盘之间切换。另外MySQL 8.0的TempTable引擎配合temptable_max_ram使用后内部临时表落盘不再是一刀切而是先尽量用内存超过阈值再逐步使用磁盘。这个参数在生产环境可以结合实例内存大小设置一般1GB到2GB比较合适。设置完可以通过SHOW VARIABLES LIKE temptable_max_ram确认生效。6. 个人经验与避坑清单6.1 我的几条使用铁律临时表不是银弹用多了反而会让数据库管理变复杂。基于我自己的实践经验我给自己定了几条铁律。第一条能用一条SQL完成就不用临时表。临时表的价值在于把“重复计算”变成“一次物化”但拆SQL本身会增加应用层和数据库之间的交互次数。如果中间结果只有几十行完全没必要建临时表直接用子查询或CTE反而更简单。第二条用临时表必须显式DROP。不管连接池会不会关闭连接不管数据库客户端会不会自动清理我都要求自己在代码里把DROP TEMPORARY TABLE写进finally块。这句话救过我两次一次是表名冲突一次是数据串了。第三条临时表字段越窄越好。临时表大部分时间在内存里VARCHAR(2000)和VARCHAR(50)对内存的占用完全不同。建表时不要图省事把源表字段原样复制过来只保留要用的字段能明显降低落盘概率。第四条永远不要让临时表和业务表同名。就算MySQL允许这也是一种极其危险的习惯。一旦ORM里有一句SELECT用到了这个表名你就有可能把普通表数据和临时表数据混淆排查成本极高。6.2 一个小技巧临时表做数据质量核对最后分享一个我很常用的小技巧。在做数据迁移或者数据对比时我会把源表和目标表需要核对的数据分别物化到两张临时表里再对临时表做差集计算快速定位不一致的数据。比如源表source_user有2000行目标表target_user只有1990行。你可以这样CREATE TEMPORARY TABLE tmp_src AS SELECT user_id, name, phone FROM source_user WHERE batch_id 100; CREATE TEMPORARY TABLE tmp_dst AS SELECT user_id, name, phone FROM target_user WHERE batch_id 100; SELECT s.user_id FROM tmp_src s LEFT JOIN tmp_dst d ON s.user_id d.user_id WHERE d.user_id IS NULL;这样能很快把缺失数据拉出来。因为临时表不影响源表你可以反复调整核对逻辑甚至多轮对比数据安全也有保障。我几乎每次做数据迁移都要用一次这个套路属于低成本高效率的工具型写法。MySQL临时表本身并不复杂复杂的是在什么场景用它、怎么控制它的生命周期、怎么留意落盘和复制的影响。希望这篇内容能帮你把临时表这个工具用到刀刃上少踩我当年踩过的那些坑。