union默认去重是因为隐式执行distinct操作,逐列比对整行全等(含null视为相等),需排序或哈希建临时表,性能显著低于union all。

UNION 为什么默认去重?不是“智能 dedup”,而是隐式加 DISTINCT
UNION 去重不是靠业务逻辑判断,而是数据库引擎在合并后自动执行一次全字段比对的 DISTINCT 操作。它把两个(或多个)结果集先拼起来,再逐行检查——所有列值完全相等(NULL 也视为相等),就只留一行。这和你在单个查询末尾写 SELECT ... FROM t GROUP BY a,b,c 或 SELECT DISTINCT a,b,c FROM t 的效果一致,只是语法糖封装在了 UNION 内部。
去重失败或意外丢数据?大概率是列对齐出问题
常见现象:明明两表里没有重复行,但 UNION 后行数变少;或者某条本该保留的数据消失了。这不是去重逻辑错了,而是列没对齐导致隐式转换失败或 NULL 被误判。
- 类型不兼容:比如
created_at DATETIME和insert_time TIMESTAMP在 MySQL 严格模式下无法自动转换,部分行可能被截断或转成NULL,进而让两行“看起来一样” - 字符串排序规则(collation)不同:跨库或跨表合并时,
utf8mb4_0900_as_cs和utf8mb4_general_ci对大小写/空格的处理不同,可能导致本不相等的字符串被判定为重复 - 数值精度丢失:
DECIMAL(10,2)和FLOAT混用,浮点误差会让本应不同的金额变成“相等”
解决方法始终是显式对齐:CAST(insert_time AS DATETIME)、COLLATE utf8mb4_0900_as_cs、统一用 DECIMAL。
UNION ALL 不去重,但不代表“更安全”
很多人以为换用 UNION ALL 就能避免丢数据,其实只是跳过了去重步骤——如果原始数据本身就有重复(比如同一用户在两张表里各有一条状态记录),UNION ALL 会照单全收,后续聚合或展示时反而更难排查。
- 真正要保数据完整性,得先确认业务语义:这两份数据是否天然互斥?比如
orders_2023和orders_2024可以用UNION ALL;但users和temp_users若存在 overlap,就得加WHERE过滤或用ROW_NUMBER()做主键优先级取舍 -
UNION ALL虽快,但若后续还要GROUP BY或DISTINCT,整体耗时未必更低——先拼再聚合,常不如先去重再拼
ORDER BY 必须放在整个 UNION 之后,否则直接报错
想给每个子查询单独排序再合并?SELECT * FROM a ORDER BY x LIMIT 10 UNION SELECT * FROM b ORDER BY y LIMIT 10 这种写法在所有主流数据库里都会报错,提示 ORDER BY 不允许出现在子查询中。
正确做法只有一种:(SELECT ... FROM a) UNION (SELECT ... FROM b) ORDER BY id。如果真需要“各自取 top 10 再合并”,必须用 CTE 或子查询套一层:
SELECT * FROM ( SELECT id, name FROM a ORDER BY score DESC LIMIT 10 UNION ALL SELECT id, name FROM b ORDER BY score DESC LIMIT 10 ) t ORDER BY score DESC LIMIT 20
注意:这里外层 ORDER BY 才生效,内层 ORDER BY 只在子查询内部有效,且仅当搭配 LIMIT 时才有意义。
去重机制本身很简单,麻烦的是它暴露了数据结构的真实一致性——列名相同不等于语义相同,类型看似兼容不等于值域无损。别迷信 UNION 的“自动”,它只是把隐式转换和全等判断藏起来了。










