导读:本期聚焦于椎名光创作的《UNION ALL 后如何去除重复记录的正确写法?三种方案对比详解》,敬请观看详情。两条SQL用UNION ALL拼接后结果出现重复行,直接改成UNION又会导致全表排序影响性能,这是SQL开发里常见的两难问题。本文从UNION与UNION ALL的底层执行差异讲起,分析重复记录产生的根本原因,并给出三种可行方案:外层包裹DISTINCT、ROW_NUMBER窗口函数去重以及分组聚合去重。文中对比了各方案在不同数据量、索引条件下的执行效率,说明何时该直接用UNION、何时保留UNION ALL再单独处理,同时提醒多字段NULL值比较、字符集不一致等容易导致去重失败的坑,帮助你在实际业务中写出既正确又高效的去重SQL。

在报表查询和多数据源合并的场景中,UNION ALL 是使用频率极高的操作符。它的优点是直接拼接结果集、不去重、速度快,但代价也很明显:一旦两个子查询存在交集,重复记录就会原样出现在最终结果里。不少人在遇到重复数据时的第一反应是把UNION ALL改成UNION,结果查询从毫秒级变成了几十秒,线上直接报警。本文就来系统讲清楚UNION ALL后去除重复记录的几种正确写法,以及每种写法适合的场景。

UNION ALL 后如何去除重复记录的正确写法?三种方案对比详解

先弄清楚重复记录是怎么产生的

UNION ALL 的语义是"全部保留",它把左右两个结果集逐行拼接,不做任何比较。重复记录的出现通常有三种原因:一是业务上两个子查询的过滤条件本身存在重叠,比如一个查"本月订单"、另一个查"金额大于1000的订单",同一条订单同时满足两个条件就会被查出来两次;二是数据源本身有重复,比如中间表没有唯一约束;三是两个子查询来自不同的表但业务主键相同,合并时自然出现重复。

所以处理重复之前,第一步应该确认重复的定义:是所有字段完全相同才算重复,还是只要业务主键相同就算重复?这两者的处理方式完全不同。所有字段相同可以直接用DISTINCT解决;而业务主键相同但其他字段可能不同的情况,则需要明确保留哪一条,通常按时间取最新或按某个优先级取第一条。

方案一:外层使用DISTINCT去重

最直接的写法是在UNION ALL外面套一层SELECT DISTINCT,这是兼容性最好、最容易理解的方案:

SELECT DISTINCT * FROM (
    SELECT order_id, user_id, amount, create_time
    FROM t_order
    WHERE create_time >= '2024-01-01'
    UNION ALL
    SELECT order_id, user_id, amount, create_time
    FROM t_order_history
    WHERE create_time >= '2024-01-01'
) t;

DISTINCT会对所有查询字段做去重,等价于UNION的默认行为,但在很多数据库中,这种写法给了优化器更多调整空间。需要注意两点:第一,DISTINCT认为两个NULL相等,所以如果字段里有NULL,行为与预期一致;第二,如果两个子查询的字段类型或字符集不一致,比如一个是utf8mb4一个是utf8,看起来相同的值可能被判为不同,去重会失效,这也是很多人踩过的坑。

方案二:ROW_NUMBER窗口函数按业务主键去重

当重复记录并不是所有字段都相同时,DISTINCT就无能为力了。比如订单表和订单历史表合并后,同一个order_id出现两次,但create_time不同,此时需要保留最新的一条。窗口函数是处理这类问题的标准做法:

SELECT order_id, user_id, amount, create_time
FROM (
    SELECT order_id, user_id, amount, create_time,
           ROW_NUMBER() OVER (
               PARTITION BY order_id
               ORDER BY create_time DESC
           ) AS rn
    FROM (
        SELECT order_id, user_id, amount, create_time
        FROM t_order
        UNION ALL
        SELECT order_id, user_id, amount, create_time
        FROM t_order_history
    ) t
) x
WHERE rn = 1;

这种写法的优势在于去重规则完全可控:PARTITION BY定义什么是"同一组",ORDER BY定义组内保留哪一条。如果两个表有优先级关系,比如实时表优先于历史表,可以加一个来源字段参与排序。窗口函数需要MySQL 8.0及以上版本支持,老版本可以用相关子查询或自连接模拟,但性能会明显下降。

方案三:分组聚合取代表记录

如果不需要保留明细,只想要每个主键的汇总结果,GROUP BY是更高效的选择。比如统计每个用户在两个表中的总金额:

SELECT user_id, SUM(amount) AS total_amount
FROM (
    SELECT user_id, amount
    FROM t_order
    WHERE status = 1
    UNION ALL
    SELECT user_id, amount
    FROM t_order_history
    WHERE status = 1
) t
GROUP BY user_id;

这种方式下重复记录反而无所谓,因为聚合操作天然会合并它们。反过来提醒一点:如果你的SQL里已经对主键做了GROUP BY,那么外层根本不需要再去重,重复添加DISTINCT只会白白增加一次排序或哈希操作。

直接用UNION还是先UNION ALL再处理

UNION的内部实现等价于UNION ALL加去重,多数数据库用排序或哈希来完成。在数据量小、或者去重列上有索引可以利用时,UNION的性能损耗并不大,直接改写成UNION是最省事的方案。但当两个子查询结果集都很大时,去重操作可能触发磁盘临时表,此时反而应该思考为什么会有重复:如果重复来自过滤条件重叠,更优的做法是调整条件让两个子查询互斥,例如给第二个子查询加上NOT EXISTS排除已查过的主键,从源头上消除重复。

SELECT order_id, user_id FROM t_order
WHERE create_time >= '2024-01-01'
UNION ALL
SELECT order_id, user_id FROM t_order_history o
WHERE o.create_time >= '2024-01-01'
  AND NOT EXISTS (
      SELECT 1 FROM t_order t WHERE t.order_id = o.order_id
  );

总结一下选择思路:数据量小直接用UNION;重复记录字段完全一致用DISTINCT;主键相同但需挑选记录用ROW_NUMBER;需要汇总用GROUP BY;能通过条件改造消除重复则优先改造条件。去重方案没有绝对优劣,关键在于先搞清楚重复产生的原因和去重的业务定义,再结合数据量和索引情况选择写法,这样才能写出既正确又高效的SQL。

UNION ALL去重SQL优化修改时间:2026-09-11 14:06:50

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0911/54704.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。