UNION和UNION ALL都是SQL中合并多个查询结果的操作,但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