UNION 比 UNION ALL 慢,因其需执行去重(DISTINCT)操作,涉及排序、分组与比对,即使子查询结果天然不重复;实测百万行场景下可能慢3–8倍,尤其无索引或内存不足时更显著。

UNION 比 UNION ALL 慢,是因为去重开销
UNION 会自动对结果集做去重(DISTINCT),这意味着数据库必须对全部中间结果排序、分组、比对——哪怕你明确知道两个子查询结果天然不重叠。实测中,一个含百万行的 UNION 查询可能比 UNION ALL 多花 3–8 倍时间,尤其在没索引或内存不足时更明显。
常见错误现象:EXPLAIN 显示 Using temporary; Using filesort;执行时磁盘 IO 突增;SHOW PROCESSLIST 中状态长期卡在 Copying to tmp table。
- 确认业务是否真需要去重:比如
(SELECT id FROM orders WHERE status = 'paid') UNION (SELECT id FROM refunds),若订单 ID 不可能同时出现在两表,就该直接换UNION ALL - 如果必须去重,优先把去重逻辑下推到子查询内部,例如用
GROUP BY id或DISTINCT提前收敛,而不是依赖 UNION 的全局去重 - 避免在 UNION 各分支里 SELECT *,字段越多、去重代价越高;只选真正需要的列,尤其是避开
TEXT、BLOB类大字段
嵌套查询里用 UNION,常因执行计划失控变慢
当 UNION 出现在子查询中(比如 WHERE id IN (SELECT id FROM a UNION SELECT id FROM b)),优化器容易放弃使用索引,转而走全表扫描或临时表。这不是 UNION 本身的问题,而是嵌套层级让优化器无法准确估算行数和选择性。
使用场景举例:从学生表和教师表合并查所有“张姓”人员 ID,再关联课程表统计选课数——这种逻辑若写成子查询 + UNION,性能往往崩塌。
- 把 UNION 拆出来,先存进临时表(如
CREATE TEMPORARY TABLE tmp_ids AS (SELECT id FROM student WHERE name LIKE '张%' UNION SELECT id FROM teacher WHERE name LIKE '张%')),再用JOIN替代IN - 用
WITHCTE 预计算(PostgreSQL / MySQL 8.0+):CTE 能让优化器更清晰地识别中间结果规模,有时可触发物化(materialization) - 检查各分支的
WHERE条件是否都命中了索引;任一分支缺失索引,整个 UNION 就可能被拖慢
替代 UNION 的三种更稳写法
不是所有 UNION 都该被删掉,但多数能被更可控的方式替代。关键看数据来源、去重要求、以及是否需支持后续 JOIN。
- 用
LEFT JOIN ... ON ... OR ...合并多源:适合主表固定、辅表条件简单的场景,避免生成中间结果集 - 用
EXISTS替代IN (UNION ...):例如WHERE EXISTS (SELECT 1 FROM student s WHERE s.id = t.student_id AND s.name LIKE '张%') OR EXISTS (SELECT 1 FROM teacher t2 WHERE t2.id = t.student_id AND t2.name LIKE '张%'),可利用索引快速短路 - 用
UNION ALL + GROUP BY显式控制去重时机:比 UNION 更透明,也方便加HAVING过滤,且某些引擎对GROUP BY的优化比 UNION 更成熟
容易被忽略的隐式陷阱
很多人改完 UNION 就以为结束了,但实际慢点常藏在细节里:字段类型不一致导致隐式转换、NULL 值参与 UNION 后影响索引选择、甚至字符集不同让合并阶段额外做转换。
- 检查每个 UNION 分支的对应列:类型、长度、字符集、是否允许 NULL 必须严格一致;否则会触发
CONVERT操作,索引失效 -
ORDER BY和LIMIT要放在最外层,不要写在单个分支里——分支内加 LIMIT 可能丢数据,外层加又无法提前剪枝 - MySQL 5.7 及更早版本不支持在子查询中直接用
UNION作为派生表(Derived Table),会强制物化且无索引,升级或改用临时表是唯一解

















