UNION 默认创建内存临时表并加锁去重,而 UNION ALL 直接流式拼接结果、不建临时表、不加锁;能否替换取决于业务是否真需去重,若无需则应优先用 UNION ALL。

UNION 操作默认触发内存临时表 + 去重逻辑,这会强制加锁并可能落盘,而 UNION ALL 完全绕过这一整套机制。
UNION 为什么会隐式创建临时表并加锁
MySQL 执行 UNION 时,必须确保结果集无重复行。它实际执行流程是:先建一个内存临时表(CREATE TEMPORARY TABLE),再把第一个子查询结果插入;接着对第二个子查询的每一行,都去和临时表中所有行做全量比对(DISTINCT 检查);最后才返回合并后的结果。这个过程天然需要写入、读取、校验临时表——只要涉及写操作,InnoDB 就会对该临时表加内部排他锁(LOCK_X),且在事务未结束前不释放。
- 即使两个子查询都只读,
UNION本身仍需写临时表,触发锁行为 - 若临时表超出
tmp_table_size和max_heap_table_size中较小值,会立刻落盘为 MyISAM 临时表,进一步加剧锁竞争和 I/O 开销 - 如果子查询含
TEXT/BLOB字段或列宽过大,也会强制落盘,锁粒度从行级升为表级
UNION ALL 是如何跳过锁和临时表的
UNION ALL 不做任何去重,MySQL 直接将各子查询结果流式拼接后返回客户端,全程不创建临时表、不写中间数据、不扫描已有结果。这意味着:
- 没有临时表创建/写入动作 → 避免了
LOCK_X对临时表的持有 - 每个子查询独立执行,可并行扫描(取决于存储引擎和隔离级别)
- 不会因内存不足而落盘 → 杜绝磁盘临时表引发的锁升级与性能断崖
- 执行计划里看不到
Using temporary,EXPLAIN输出更干净
什么时候不能直接换 UNION ALL
不是所有 UNION 都能无脑替换成 UNION ALL,关键看业务是否真需要去重。但很多场景下“以为需要”,其实并不需要:
- 子查询本身已通过
WHERE或主键约束保证无交集(例如SELECT * FROM t1 WHERE type=1UNIONSELECT * FROM t1 WHERE type=2)→ 可安全换 - 外层已用
DISTINCT或应用层做了去重 → 内层UNION属于冗余去重,应删掉 - 子查询含
GROUP BY或聚合,结果天然唯一 →UNION的去重毫无意义 - 若确实要保留去重,优先考虑改写为
SELECT DISTINCT ... FROM (subquery1 UNION ALL subquery2) AS t,把去重下推到最终一步,避免中间临时表膨胀
改用 UNION ALL 后仍被锁?检查这些隐藏点
即便用了 UNION ALL,如果还看到锁等待或 SHOW PROCESSLIST 中出现 Locked 状态,问题大概率不在 UNION 本身,而是子查询里的操作:
- 子查询中含
FOR UPDATE、LOCK IN SHARE MODE→ 锁由这些显式语句触发,和UNION ALL无关 - 子查询更新了同一张表(比如自关联更新),导致 InnoDB 行锁冲突 → 查
INNODB_TRX和INNODB_LOCK_WAITS定位具体锁资源 - 子查询用了
ORDER BY+LIMIT但没走索引 → 触发文件排序(Using filesort),可能临时占用更多内存甚至磁盘,间接拉长事务时间,放大锁等待 - 连接未设置
AUTOCOMMIT=1,且多个UNION ALL查询包在同一个事务里 → 锁会持续到事务结束,不是UNION的锅,是事务设计问题
真正容易被忽略的是:UNION ALL 虽不加临时表锁,但它无法消除子查询自身带来的锁。优化必须落到每个子查询的执行路径上,而不是只盯着 UNION 关键字。


















