PostgreSQL中UNION与UNION ALL效率到底差多少?

来源:SpringBoot教程作者:广州GEO公司头衔:草根站长
导读:本期聚焦于广州GEO公司创作的《PostgreSQL中UNION与UNION ALL效率到底差多少?》,敬请观看详情。合并多条查询结果时,UNION与UNION ALL的成本差异经常被低估。UNION为了保证结果行唯一,必须对全部输出进行去重,通常会伴随排序或哈希聚合;UNION ALL则不做任何额外处理,相当于把多个结果集直接追加返回。PostgreSQL优化器在两种写法下生成的执行计划差异明显,数据量越大,UNION带来的CPU和I/O开销越突出。本文结合EXPLAIN ANALYZE和实际查询场景,对比二者在百万行数据下的表现,并说明如何根据是否需要去重来选择合适的集合操作。掌握这个区别,往往只改一个关键字就能显著降低查询延迟。

UNION和UNION ALL都是SQL中合并多个查询结果的操作,但PostgreSQL对两者的处理路径差异很大。UNION会执行去重,意味着数据库必须把来自不同子查询的所有行收集起来,判断哪些行是重复的,再去掉重复项;UNION ALL则不会进行任何去重操作,它只是把各个子查询的结果按顺序追加在一起返回。这个语义上的区别反映在执行计划中,直接决定了两种写法的资源消耗和运行速度。

PostgreSQL中UNION与UNION ALL效率到底差多少?

如果只需要合并结果而不关心是否存在重复行,UNION ALL几乎是更优的选择。但在实际业务中,有时必须依靠UNION来保证结果唯一,因此理解它们的内部执行机制,比单纯记住“UNION ALL更快”更加重要。

一、执行计划差异:Append与去重节点

在PostgreSQL中,UNION ALL的执行计划通常非常简单。优化器会生成一个Append节点,把多个子查询的结果集顺序连接。Append节点本身不会比较行与行之间的关系,也不会缓存完整结果,因此可以做到边读取边返回。只要上游子查询支持流式读取,UNION ALL的响应时间和资源开销基本接近单独执行各个子查询的总和。

UNION则完全不一样。为了去除重复行,PostgreSQL必须在Append之后增加一个去重节点。去重可以通过HashAggregate或Sort加Unique实现,具体选择取决于优化器的成本估算、数据类型和work_mem参数设置。无论采用哪种方式,这个节点都需要看到全部输入行之后才能输出第一行结果,因此UNION查询通常具有更高的启动延迟,并且在数据量较大时会占用明显更多的内存或磁盘空间。

举个简单例子。假设有两张表t1和t2,分别保存1到100000以及50001到150000的整数。重复区间为50001到100000,共约50000行。下面的查询用来对比两种写法。

CREATE TABLE t1 AS
SELECT generate_series(1, 100000) AS id;

CREATE TABLE t2 AS
SELECT generate_series(50001, 150000) AS id;

EXPLAIN ANALYZE
SELECT id FROM t1
UNION
SELECT id FROM t2;

执行UNION时,计划中通常会出现HashAggregate节点,负责对合并后的200000行做去重,最终返回150000行。而如果改成UNION ALL,执行计划里只会看到Append节点,不再有额外的去重步骤。

EXPLAIN ANALYZE
SELECT id FROM t1
UNION ALL
SELECT id FROM t2;

上面的对比可以直观说明:多出的去重节点不仅消耗CPU,还可能触发磁盘临时文件。对于只需要合并、不要求唯一性的查询,UNION ALL的计划更加简洁。

二、去重机制如何消耗内存和CPU

PostgreSQL实现UNION去重时,比较常见的策略是哈希聚合。数据库会为结果集维护一个哈希表,每读入一行就计算哈希值,并检查是否已经存在。如果存在就丢弃该行,否则将其插入哈希表并输出。哈希表的大小与输入行数和行的宽度成正比。当work_mem不足以容纳整个哈希表时,PostgreSQL会把部分数据写入磁盘临时文件,后续再通过多轮合并完成去重,这个过程会显著增加I/O开销。

另一种策略是先对全部结果排序,再扫描排序结果,跳过相邻的重复行。排序去重的时间复杂度通常高于哈希聚合,但它在某些场景下可以借助索引或已经有序的子查询减少成本。不过对于UNION来说,子查询之间往往没有整体顺序,排序去重通常比哈希聚合更昂贵,所以优化器在统计数据允许时更倾向于哈希聚合。

可以通过EXPLAIN参数观察这些资源消耗。下面的语句会展示内存使用和临时文件信息。

SHOW work_mem;

SET work_mem = '64MB';

EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT id FROM t1
UNION
SELECT id FROM t2;

如果输出中出现Disk: ...之类的字样,说明去重过程已经溢出到磁盘,这时查询延迟往往会成倍增长。增大work_mem可以缓解哈希表溢出,但更高的内存占用会影响其他并发查询。UNION ALL则不存在这种两难,因为它不需要维护全局哈希表或排序缓冲区。

三、如何选择以及性能优化建议

第一条原则是先确认业务是否真的需要去除重复行。如果两个子查询来自互斥的数据源,例如按年份拆分的分区表、不同区域的用户表,或者一个查询返回偶数、另一个返回奇数,那么结果天然没有交集,使用UNION只会白白付出去重成本。此时应直接写UNION ALL,既减少执行时间,也降低资源消耗。

如果结果中可能存在重复,但重复行对最终结果没有影响,比如后面还要进行聚合、排序或分页,UNION ALL常常仍然更合适。可以在UNION ALL的外层根据需要再做一次GROUP BY或DISTINCT。比如下面这种写法,先合并再去重,能够把去重操作集中在一个明确的位置,便于索引或并行策略发挥作用。

SELECT id
FROM (
    SELECT id FROM t1
    UNION ALL
    SELECT id FROM t2
) AS merged
GROUP BY id;

当然,外层GROUP BY不一定总是比直接UNION快,改写前建议用EXPLAIN ANALYZE对比实际执行计划。关键点是:UNION的去重是隐式的,往往不容易被优化;显式聚合则给了数据库更多优化空间,也更容易被后续维护者理解。

另外还要注意列的数量和宽度。UNION去重的成本与结果行的大小直接相关,只查询必要列,避免把大字段带入集合操作,能显著减少哈希表体积。对于可能重复的数据,提前在子查询中用WHERE条件缩小范围,也能减轻去重压力。

四、容易踩到的几个坑

第一个坑是误以为UNION会自动排序。UNION只会去重,并不保证输出顺序。即便某些版本的PostgreSQL在特定计划下看起来返回了有序结果,那也只是执行计划的副作用,不能依赖。需要排序时必须显式使用ORDER BY,同样,UNION ALL也不会继承子查询的ORDER BY,除非在子查询中使用括号和LIMIT等特殊写法。

第二个坑忽略了NULL在去重中的行为。UNION去重时把多个NULL视为相同值,因此两行如果只有NULL值,最终只保留一行;而UNION ALL会原样保留所有NULL行。这个差异在数据质量校验或日志合并时可能会影响统计结果。

第三个坑是在视图或复杂查询中隐藏了UNION。有时看起来很简单的报表查询,底层视图可能是多个UNION嵌套组成。此时单独优化某条SQL可能效果有限,应当查看视图定义,确认哪些UNION可以安全改为UNION ALL,或者通过物化视图预处理来减少重复计算。

总的来说,UNION和UNION ALL的选择不仅是语法差异,更是性能决策。默认情况下,如果不需要去重,就应优先使用UNION ALL;只有在结果唯一性是业务硬性要求时,才让PostgreSQL承担去重成本。

PostgreSQL UNIONUNION ALL查询性能优化修改时间:2026-08-25 08:58:15

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