登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  MySQL

MySQL 8.4 UNION ALL 为什么比 UNION 稳:去重临时表与排序开销边界

来源:17golang原创

时间:2026-08-27 19:58:30 395浏览 收藏

报表上线后,订单列表突然多了一层“合并历史数据”的查询。业务确认两张表不会存同一批订单,可 SQL 写成 UNION 后,慢日志里却出现了临时表和额外排序。这个场景里,真正要判断的不是“结果有没有重复”,而是数据库是否被要求为你做重复消除。

如果两段查询的结果允许重复,优先使用 UNION ALL;只有确实需要按整行去重时才用 UNION,并用执行计划确认去重代价落在哪里。

要点速览
  • UNION ALL 直接追加两段结果,不主动做整行去重。
  • UNION 需要比较两段结果的完整行,可能引入去重临时表和排序。
  • 列数量、顺序和类型要先对齐,别把“业务唯一”误写成“SQL 自动唯一”。
  • 验收要同时看结果行数、EXPLAIN 和临时表指标,不能只看一次耗时。

订单历史合并为什么会多出一段排序

准备两张结构一致的表:当前订单表 orders_current 保存近三个月数据,归档表 orders_archive 保存已归档数据。两段查询只取报表需要的四列:

SELECT order_id, user_id, status, created_at
FROM orders_current
WHERE user_id = 9001
UNION
SELECT order_id, user_id, status, created_at
FROM orders_archive
WHERE user_id = 9001;

即使应用层已经按时间把两段表分开,UNION 仍按结果行判断重复。它不知道“当前表和归档表在业务上互斥”这条约定,必须先完成 duplicate removal,再把结果交给外层排序或分页。

这里先别急着给索引加列。第一步是确认合并操作本身是否多做了工作。

UNION ALL 和 UNION 的真实执行路径

把同一条件改成 UNION ALL,语义变成“按顺序追加两段结果”。它不替你证明订单唯一,也不替你消除重复;好处是查询可以更直接地把 orders_currentorders_archive 的结果送入后续处理。

UNION 会在合并阶段增加 duplicate removal。当结果集较大、列较宽,或外层还有 ORDER BY created_at 时,去重与排序都可能消耗内存并溢出到磁盘临时表。图中的节点是这条 SQL 实际会经过的判断链:

MySQL UNION ALL 从 orders_current 和 orders_archive 追加结果并跳过去重的查询路径

对照实验可以只替换一个词:

-- 两张表按业务边界互斥:保留重复行语义
SELECT order_id, user_id, status, created_at
FROM orders_current
WHERE user_id = 9001
UNION ALL
SELECT order_id, user_id, status, created_at
FROM orders_archive
WHERE user_id = 9001;

-- 需要整行去重时才使用 UNION
SELECT order_id, user_id, status, created_at
FROM orders_current
WHERE user_id = 9001
UNION
SELECT order_id, user_id, status, created_at
FROM orders_archive
WHERE user_id = 9001;

先用三项检查确认是否真的适合 UNION ALL

检查列定义,而不是只看列名

两段查询必须返回相同数量的列,位置对应的数据类型也要能安全合并。第一段的列名通常决定结果列名;如果一个分支把 created_at 转成字符,外层排序就可能出现隐式转换。

SELECT order_id, user_id, status, created_at
FROM orders_current
UNION ALL
SELECT order_id, user_id, status, created_at
FROM orders_archive;

检查“重复”是不是业务上可接受

如果归档动作是复制而不是迁移,同一个 order_id 可能同时出现在两张表。此时 UNION ALL 会忠实返回两行;它不是错误,也不会因为存在主键就替你去重。要保留哪一行,必须写出明确规则,例如按 created_at 取最新记录。

检查外层排序和分页

UNION ALL 只解决合并阶段的额外去重,不能保证整条 SQL 不排序。跨两张表统一按时间排序时,仍需要在最外层写 ORDER BY,分页还应补上稳定的第二排序键:

SELECT order_id, user_id, status, created_at
FROM (
  SELECT order_id, user_id, status, created_at FROM orders_current WHERE user_id = 9001
  UNION ALL
  SELECT order_id, user_id, status, created_at FROM orders_archive WHERE user_id = 9001
) AS merged_orders
ORDER BY created_at DESC, order_id DESC
LIMIT 50;

用 EXPLAIN 和结果行数做一次可复查验收

不要只比较客户端看到的耗时。对两个版本分别执行 EXPLAIN,记录每个分支的访问方式、估算行数和外层排序;再用计数查询核对结果是否符合业务预期。

MySQL UNION 增加 duplicate removal 和 sort 后的临时表资源路径,与 UNION ALL 的追加路径对照

EXPLAIN
SELECT order_id, user_id, status, created_at FROM orders_current WHERE user_id = 9001
UNION ALL
SELECT order_id, user_id, status, created_at FROM orders_archive WHERE user_id = 9001;

SELECT COUNT(*) AS all_rows
FROM (
  SELECT order_id FROM orders_current WHERE user_id = 9001
  UNION ALL
  SELECT order_id FROM orders_archive WHERE user_id = 9001
) AS all_result;

SELECT COUNT(*) AS distinct_rows
FROM (
  SELECT order_id FROM orders_current WHERE user_id = 9001
  UNION
  SELECT order_id FROM orders_archive WHERE user_id = 9001
) AS distinct_result;

如果 all_rows 明显大于 distinct_rows,说明两张表确实有重复键,不能只凭“归档表应该互斥”的设计文档切换。若两者长期相等,再结合归档约束和抽样数据,才有理由选择追加语义。

这几个坑会让 UNION ALL 的收益消失

  • UNION ALL 当成去重版本:它不会删除重复行。
  • 在每个子查询里分别写 ORDER BY,却没有配合 LIMIT;最终顺序仍由外层排序决定。
  • 用字符串拼接代替类型对齐,导致日期和数字在合并或排序时发生隐式转换。
  • 只看一次冷缓存耗时,不记录结果行数、执行计划和临时表变化。

相关问题

UNION ALL 会不会比 UNION 永远快?

不会。它少了整行去重,但外层排序、宽字段、磁盘临时表或两段扫描仍可能成为瓶颈。

两张表没有重复主键,是否必须使用 UNION ALL?

不必须。应以数据库中实际可验证的约束和迁移流程为依据;没有稳定保证时,保留 UNION 的去重语义更安全。

只想按 order_id 去重怎么办?

UNION 的去重对象是整行,不是单列。需要按 order_id 选最新记录时,应使用窗口函数或聚合明确写出取舍规则。

把选择写成一条可执行规则

两张结果集业务上允许重复、列已对齐、外层排序也有明确成本时,使用 UNION ALL;必须整行去重时使用 UNION,并把 duplicate removalsort、临时表和结果行数纳入验收。真正稳定的 SQL,不是看起来少一个关键字,而是每个语义都能由数据和执行计划对上。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>