前言
SQL 执行计划里的 Using temporary,很多人第一反应是“哦,看到了,问题不大”。但真想错了。这个标志其实意味着,排序、分组、去重这些操作,执行器需要借一张临时表来完成。数据量小的时候,这张表待在内存里,一切安好。一旦数据量上来,内存不够用,临时表就会落盘——查询延迟直接从“秒级”变成“你喝杯咖啡回来它还没跑完”。

所以,看到 Using temporary,别只皱一下眉就翻过去。它值得被认真排查。
一、临时表从哪里来
flowchart TD A[SQL] --> B[GROUP BY] A --> C[ORDER BY] A --> D[DISTINCT] A --> E[UNION] B --> F[Temporary Table] C --> F D --> F E --> F
临时表不一定是坏事,但它等于在说:“执行器需要找个地方存中间结果。”那问题就变成了:存多少数据?内存够不够?会不会落盘?
二、先看执行计划和实际执行
EXPLAINSELECT user_id, COUNT(*)FROM ordersWHERE created_at >= '2026-07-01'GROUP BY user_idORDER BY COUNT(*) DESC;
最常见的排查入口,就是 EXPLAIN。Extra 列里一旦出现 Using temporary,别急着跳过——尤其是后面还跟着 Using filesort 的时候。这时候得继续往下看扫描行数、排序行数、实际耗时。MySQL 8 里可以直接用 EXPLAIN ANALYZE,它给出的信息更接近真实执行情况,比光看一个计划靠谱得多。
三、内存临时表和磁盘临时表差很多
内存临时表还好,落盘才要命。一旦临时表被迫写到磁盘,延迟就不是线性增长了,而是直接跳一个量级。怎么判断有没有落盘?MySQL 有现成的状态变量:
SHOW GLOBAL STATUS LIKE 'Created_tmp%';
重点关注 Created_tmp_disk_tables。如果这个值随着某个查询的执行明显增长,说明临时表已经走了磁盘路径——这时候就该认真对待了。
四、优化要从 SQL 形态开始
很多时候,一个合适的索引就能把临时表去掉。比如加个复合索引:
CREATE INDEX idx_orders_created_user ON orders(created_at, user_id);
但别忘了,索引不是万能钥匙。ORDER BY COUNT(*) 这种聚合排序,本质就需要中间结果——索引能加速扫描,但排序和聚合本身还是要算一遍。不要幻想一个索引解决所有分组排序的问题。
还有一个容易被忽略的点:临时表的字段类型。如果中间结果里包含了 TEXT、BLOB 或者很宽的 VARCHAR,更容易触发磁盘临时表。所以查询里只需要少数列的时候,别把宽列带进子查询或派生表:
SELECT user_id, COUNT(*) AS cntFROM ( SELECT user_id FROM orders WHERE created_at >= '2026-07-01') tGROUP BY user_id;
这种改写不一定每次都能提速,但它提醒我们一个道理:中间结果越窄,临时表越可控。
五、总结
Using temporary 不是一句小提示。它可能来自 GROUP BY、ORDER BY、DISTINCT、UNION 这些操作,关键要看它是否落盘、扫描了多少行、实际耗时多少。
排查时,把 EXPLAIN ANALYZE、状态变量和 SQL 形态结合起来判断。该加索引就加,该改查询就改,别把临时表当空气。
如果临时表来自业务必需的大范围聚合,那问题就不在单条 SQL 上了——这时候该考虑的是数据模型或报表链路。汇总表、异步计算、分析库,都比让在线库每天重复做同一批昂贵中间结果要划算。
把计算放到合适的位置,本身就是优化。
别让在线查询承受离线报表的重量。
