广告位 · 页头(后台-模块管理-新建模块后替换此区域)

如何分析 Extra 字段中的 Using temporary?临时表产生原因与消除方法

在 MySQL 的执行计划中,Extra 列出现 Using temporary,说明查询在执行过程中需要创建内部临时表来存储中间结果。它和 Using filesort 经常一起出现,但二者的根因并不完全相同:filesort 表示需要额外排序,而 temporary 表示优化器无法在一次扫描中直接完成去重、分组、排序或结果合并。判断这个标记是否需要处理,不能只看标记本身,还要结合临时表是否落盘、扫描数据量以及业务对延迟的容忍度。

如何分析 Extra 字段中的 Using temporary?临时表产生原因与消除方法

理解内部临时表的工作机制,是分析问题的前提。很多人看到 Using temporary 便急着调大 tmp_table_size,但临时表可能仍然会写磁盘,因为是否落盘不仅取决于结果集大小,还取决于字段类型、存储引擎支持的列类型以及 SQL 语义。下面从产生原因、定位方法、消除手段和监控评估几个层面展开。

一、Using temporary 的底层含义与常见触发场景

MySQL 内部临时表并不等同于用户显式创建的 CREATE TEMPORARY TABLE。内部临时表由优化器自动管理,通常分为内存临时表和磁盘临时表两类。内存临时表默认使用 MEMORY 引擎,受 tmp_table_size 与 max_heap_table_size 两个参数共同限制;一旦中间结果超过阈值,或者包含 MEMORY 引擎不支持的 TEXT、BLOB 等大字段类型,MySQL 就会把临时表转到磁盘。对于临时表是否落盘,仅从 Extra 列的 Using temporary 无法直接判断,需要结合 SHOW SESSION STATUS LIKE 'Created_tmp%'; 中的 Created_tmp_disk_tables 增长情况,或者使用 EXPLAIN ANALYZE 观察实际执行。

Using temporary 最典型的场景是:结果集需要先保存下来,再经过二次处理。比如 GROUP BY 的列与 ORDER BY 的列不同,优化器可能先分组得到中间结果,再对聚合结果排序;又比如对没有合适索引的列做 DISTINCT 后还要按其他列排序;还有 UNION 不带 ALL 时需要对两个结果集去重合并。子查询被物化为派生表、窗口函数计算、某些复杂 HAVING 过滤也可能触发临时表。它并不一定代表查询很慢,但没有必要地生成大临时表,尤其磁盘临时表,会显著增加 I/O 和内存压力。

常见的触发条件可以归纳为:

  • GROUP BY 与 ORDER BY 字段不一致,或聚合结果需要二次排序;
  • 对非索引列使用 DISTINCT,同时又需要按其他列排序;
  • 使用 UNION 而不是 UNION ALL,强制执行去重;
  • 子查询被物化后参与关联,而派生表缺少有效索引;
  • 窗口函数或复杂 HAVING 需要先缓存行集再计算。

排查前可以先观察当前连接创建临时表的情况:

SHOW SESSION STATUS LIKE 'Created_tmp%';

二、通过执行计划定位临时表产生原因

要定位是哪一步产生了临时表,不能只看传统 EXPLAIN 的 Extra 列,建议使用 EXPLAIN FORMAT=JSON。JSON 格式会给出 using_temporary_table、using_filesort、materialized_from_subquery 等标记,能更清晰地看到临时表的来源。例如下面是一张订单表:

CREATE TABLE orders (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT NOT NULL,
  product_id BIGINT NOT NULL,
  status TINYINT NOT NULL,
  amount DECIMAL(10,2) NOT NULL,
  created_at DATETIME NOT NULL,
  KEY idx_user (user_id, status),
  KEY idx_product (product_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

执行这样一个汇总查询:

EXPLAIN
SELECT product_id, COUNT(*) AS cnt
FROM orders
GROUP BY product_id
ORDER BY cnt DESC
LIMIT 10;

执行计划的 Extra 通常会显示 Using temporary; Using filesort。原因在于 GROUP BY product_id 可以借助 idx_product 索引流式扫描,但 ORDER BY cnt 是对聚合结果排序,而 cnt 不是表中的原始列,优化器只能先把每个 product_id 的计数结果保存到临时表,再根据计数值排序。这类查询在数据量较大时,临时表会明显影响响应时间。

再看一个 UNION 示例:

EXPLAIN
SELECT user_id FROM orders WHERE status = 1
UNION
SELECT user_id FROM orders WHERE status = 2;

由于 UNION 默认要去除两个结果集中的重复行,MySQL 需要先将两路结果写入临时表,再根据所有列进行去重。改成 UNION ALL 后不再需要去重,执行计划中的 Using temporary 往往会消失。使用 EXPLAIN FORMAT=JSON 查看时,会看到类似 using_temporary_table 为 true 的标记,可以帮助你锁定具体查询块。

三、消除 Using temporary 的实用思路

优先从索引和 SQL 语义入手,而不是简单调大内存参数。第一种思路是让分组与排序尽量借用同一个索引。例如查询按 user_id 分组并按 user_id 排序时,如果索引前缀是 user_id,优化器可以在扫描索引时直接完成聚合,不需要额外保存分组结果。如下所示:

SELECT user_id, status, COUNT(*)
FROM orders
WHERE user_id BETWEEN 100 AND 200
GROUP BY user_id, status
ORDER BY user_id, status;

此时 idx_user(user_id, status) 同时满足过滤、分组和排序的顺序,执行计划中通常不会再出现 Using temporary。但如果排序依据是聚合值,比如按 COUNT(*) 排序,表索引就无法直接消除临时表,需要从业务上判断能否减少排序结果集,或把排序放到应用缓存中处理。

第二种思路是消除无谓去重。很多系统默认使用 UNION,实际上两个分支的数据源如果本身不重复,或者业务允许重复后再在应用层处理,应该改为 UNION ALL:

SELECT user_id FROM orders WHERE status = 1
UNION ALL
SELECT user_id FROM orders WHERE status = 2;

如果两个分支确实可能重复并且又必须去重,再考虑外层套一层 GROUP BY,这样至少可以控制临时表的大小,避免 UNION 对全字段去重带来的额外开销。

第三种思路是利用覆盖索引让 DISTINCT 和分组操作可以仅扫描索引页。例如 SELECT DISTINCT product_id FROM orders; 如果 product_id 有索引,优化器可以扫描索引并去重,不需要额外的临时表。如果去重对象是 TEXT 或 BLOB 类型,内存临时表无法容纳,会直接导致磁盘临时表。此时可以考虑对内容做哈希后再去重:

SELECT DISTINCT SHA2(content, 256) AS content_hash
FROM articles;

这种改写只适用于只需判断内容是否重复、不需要返回原文的场景。参数调整只能作为辅助手段,不能从根本上消除临时表。比如适当调大 tmp_table_size 和 max_heap_table_size,可以让更多中间结果留在内存中完成:

SET SESSION tmp_table_size = 64 * 1024 * 1024;
SET SESSION max_heap_table_size = 64 * 1024 * 1024;

但这会增加单会话的内存占用,如果并发较高,可能引发更大的内存压力。真正的优化仍然要回到 SQL 改写和索引设计上。

四、临时表并非总是敌人:何时保留与监控

Using temporary 并不总是需要消除。在某些聚合场景下,创建内部临时表是正常且高效的执行方式,尤其是结果集较小、临时表在内存中完成时,对整体性能影响几乎可以忽略。真正需要警惕的是磁盘临时表频繁产生,或者临时表行数超过预期。通过 EXPLAIN ANALYZE 可以看到更真实的执行成本:

EXPLAIN ANALYZE
SELECT product_id, COUNT(*) AS cnt
FROM orders
GROUP BY product_id
ORDER BY cnt DESC
LIMIT 10;

输出中会包含实际执行时间、临时表创建行数等信息。如果发现 actual time 明显偏高,并且伴随大量磁盘临时表,就需要继续下钻。日常维护中可以通过全局状态观察临时表落盘比例:

SHOW GLOBAL STATUS LIKE 'Created_tmp%';

当 Created_tmp_disk_tables 与 Created_tmp_tables 的比值持续偏高时,说明很多临时表在使用过程中被转到了磁盘。此时不要只调参数,而应优先分析这些临时表来自哪些 SQL,检查是否存在大字段、缺少索引、无谓去重或分组排序不一致等问题。

最终处理顺序可以概括为:先通过执行计划定位产生临时表的查询块,再判断临时表是否落盘、是否真的影响性能,然后优先从 SQL 语义和索引设计上去除不必要的中间结果集,最后才考虑调整内存参数作为缓冲。这样既能避免无效优化,也能让查询更加稳定。

广告位 · 文章底部(后台-模块管理替换此区域)