UNION ALL 后如何高效去重(避免额外 DISTINCT?

来源:PHP编程网作者:香港程序员头衔:程序员
导读:本期聚焦于香港程序员创作的《UNION ALL 后如何高效去重(避免额外 DISTINCT?》,敬请观看详情。为什么 UNION ALL 查询结果出现重复数据后,很多写法会直接在 外层套一个 DISTINCT 了事?这种做法往往让数据库对全量结果集做排序或哈希去重,数据量大时性能急剧下降。本文从 UNION 与 UNION ALL 的执行差异入手,分析重复数据产生的根源,给出在源头过滤、分组聚合、窗口函数、分区裁剪等多种替代方案,并结合执行计划对比各方案的适用场景与性能表现,帮你写出既正确又高效的 SQL。

UNION ALL 是 SQL 中拼接多个结果集的常用手段,它不像 UNION 那样自动去重,因此性能开销小得多。但正因为它不去重,当多个分支的数据存在交叉时,拼接后的结果就会出现重复行。不少人的第一反应是在最外层加一个 DISTINCT,问题看似解决了,实际上数据库需要对整个结果集做哈希或排序去重,数据量一上来,CPU 和内存压力都非常可观。更合理的思路是:先搞清楚重复从哪里来,再在合适的层级把重复消掉,而不是把包袱甩给最后一步。

UNION ALL 后如何高效去重(避免额外 DISTINCT?

一、先弄清楚重复数据是怎么产生的

UNION ALL 只做简单的结果集拼接,不比对行内容。如果两个分支的查询条件有重叠,比如一个查最近七天的订单、另一个查金额大于一万的订单,某笔订单既满足时间条件又满足金额条件,就会在最终结果中出现两次。这类重复本质上是业务逻辑上的重叠,而不是数据本身有问题。

另一种情况是数据源本身就存在重复,比如日志表按天分区,同一笔业务日志因为重试被写入了多次。这时候无论怎么改 UNION ALL 的写法,重复都在源头,必须在分支内部先处理。

区分这两种场景非常重要:前者可以通过调整分支的过滤条件在拼接前消除重叠;后者则必须依赖源表层面的去重手段。盲目上 DISTINCT 属于掩盖问题,既慢又可能把本来不该去重的数据误删。

二、在分支内部提前去重,缩小外层处理的数据量

如果重复发生在某个分支内部,最直接的办法是在该分支内先做去重或聚合,把干净的数据交给 UNION ALL。常见的写法是用 GROUP BY 或者窗口函数取一条:

-- 方案一:分组聚合,适合取汇总值的场景
SELECT order_id, MAX(update_time) AS update_time, MAX(amount) AS amount
FROM order_log
GROUP BY order_id

UNION ALL

SELECT order_id, update_time, amount
FROM order_current;

-- 方案二:窗口函数取每组第一条,适合保留完整行明细
SELECT order_id, update_time, amount
FROM (
    SELECT order_id, update_time, amount,
           ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY update_time DESC) AS rn
    FROM order_log
) t
WHERE rn = 1;

这两种方式的共同点是:去重发生在数据流的上游,UNION ALL 拼接的都是已经干净的行集。相比在最终结果上做 DISTINCT,优势在于每个分支可以利用索引完成分组或排序,代价被分摊且可控。尤其当分支之间没有交叉时,拼接后的结果天然无重复,DISTINCT 完全可以省掉。

需要注意 GROUP BY 与 ROW_NUMBER 的取舍:如果只需要部分列,GROUP BY 配合聚合函数往往更快;如果要保留整行,窗口函数更直观,但要注意 PARTITION BY 的列是否有索引支持,否则全表排序的开销也不小。

三、消除分支之间的条件重叠

当重复来自分支查询条件交叉时,最优雅的方案是让每个分支只负责互斥的数据段。比如把第二个分支的条件改为排除第一个分支已经覆盖的范围:

SELECT order_id, amount
FROM orders
WHERE create_time >= CURRENT_DATE - INTERVAL 7 DAY

UNION ALL

SELECT order_id, amount
FROM orders
WHERE amount > 10000
  AND create_time < CURRENT_DATE - INTERVAL 7 DAY;

这样写虽然条件啰嗦了一点,但每个分支都能精确命中索引,且结果天然无重复,完全不需要任何去重操作。执行计划上通常表现为两个独立的索引范围扫描加一个简单的拼接,成本远低于全量哈希去重。

在分区表场景下,这种写法还有一个额外好处:各分支的过滤条件可以触发分区裁剪,只扫描必要的分区。而外层 DISTINCT 会导致数据库必须先物化全部结果再去重,分区裁剪带来的收益被大幅削弱。

四、确实需要跨分支去重时的替代方案

有些场景重复跨分支存在,且无法通过互斥条件拆分,比如合并多张结构相同的表后按主键取最新版本。这时与其用 DISTINCT,不如用 ROW_NUMBER 统一编号:

SELECT order_id, amount, source
FROM (
    SELECT order_id, amount, 'history' AS source,
           ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY update_time DESC) AS rn
    FROM (
        SELECT order_id, amount, update_time FROM orders_2023
        UNION ALL
        SELECT order_id, amount, update_time FROM orders_2024
    ) merged
) ranked
WHERE rn = 1;

这种写法的好处是去重规则明确——按 update_time 保留最新一条,而不是 DISTINCT 那种简单的整行比对。它还能避免一个隐蔽的坑:DISTINCT 要求所有列完全相同才算重复,如果两个分支同一笔订单的某个字段略有差异,DISTINCT 会把两行都保留下来,结果依然是错的。

从执行计划的角度看,DISTINCT 通常对应 Hash Aggregate 或 Sort 运算符,需要把整个输入装载进内存哈希表,内存不足时会落盘。而 ROW_NUMBER 虽然也要排序,但排序键更少,且通常能利用分支上的索引提前完成。两种方式在中小数据量上差距不明显,数据量到千万级时差距可能达到数倍。

五、总结

UNION ALL 后的去重问题,核心原则是把去重动作尽量前移:能改写条件消除重叠的,改条件;重复在分支内部的,分支内先聚合;确需跨分支去重的,用窗口函数明确定义保留规则。外层 DISTINCT 应该是确认无计可施后的兜底手段,而不是默认选项。写 SQL 时多花十分钟分析重复的来源,往往能让查询性能提升一个量级,也能避免 DISTINCT 误删或多留数据带来的正确性风险。

UNION ALLSQL去重DISTINCT性能优化修改时间:2026-09-09 12:10:57

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