PostgreSQL与SQL Server中GROUP BY HAVING EXISTS两次扫描原因探究
聚合查询重复扫描表的底层逻辑与合理性解析
问题背景
在PostgreSQL测试用例(postgres/src/test/regress/sql/aggregates.sql:1019)中有如下查询:
select ten, sum(distinct four) filter (where four > 10) from onek a group by ten having exists (select 1 from onek b where sum(distinct a.four) = b.four);
onek表包含1000行数据,查询逻辑为:
- 扫描onek表a,按
ten分组,对four列带过滤条件(four > 10)去重求和 - 通过
HAVING EXISTS半连接onek表b,子查询中引用了a表另一个无过滤的sum(distinct a.four)聚合
查看PostgreSQL的执行计划,以及SQL Server中用CASE替代FILTER改写后的执行计划,均发现引擎对a表进行了两次扫描、两次GROUP BY,分别计算两个不同聚合后再关联,最后与b表做半连接。
作为SQL数据库引擎开发者,核心疑问:
- 重复扫描的原因是什么?
- 为何不将第二个聚合逻辑合并到原a表的扫描中?
- 数据库引擎此行为的合理性是什么?
核心原因与合理性分析
1. 聚合函数的上下文与作用域限制
SQL标准中,HAVING子句里的聚合函数和SELECT列表中的聚合函数,虽基于同一分组结果,但在查询解析阶段被视为两个独立的聚合计算需求。
- SELECT列表中的
sum(distinct four) filter (where four >10)是带过滤条件的去重求和 - HAVING子查询中的
sum(distinct a.four)是无过滤的去重求和
子查询的上下文是独立的,当前主流优化器难以识别这两个聚合可共享分组后的基础数据集,因此会将它们拆分为独立的计算步骤。
2. 去重聚合的计算特性限制
sum(distinct ...)属于去重型聚合,这类聚合需先对分组内目标列做去重处理再求和:
- 带FILTER条件的聚合:先过滤不符合条件的行,再去重求和
- 无FILTER的聚合:直接对所有行去重求和
两者的计算步骤顺序不同,若要合并计算,优化器需额外处理分组内数据的多维度去重与过滤逻辑,大幅增加实现复杂度。对于小表(如1000行的onek),两次扫描的性能开销远低于优化器开发合并逻辑的成本。
3. 半连接的逻辑独立性
HAVING EXISTS中的子查询是独立的半连接逻辑,优化器会先计算外层分组的聚合结果,再将结果带入子查询匹配。若要合并两个聚合,需将子查询中的聚合提前到外层分组阶段计算,这涉及严格的语义等价性重写,一旦处理不当会导致结果错误。为规避语义风险,优化器选择更保守的独立扫描策略。
4. 性能与实现的权衡
对于小表,两次扫描的性能差异可忽略;对于大表,当前主流优化器仍倾向于独立扫描,原因在于:
- 合并逻辑需处理大量边界情况(如NULL值、多过滤条件组合等),实现复杂度极高
- 独立扫描的执行计划更简单,生成与验证成本低,减少优化器决策时间
- 独立扫描的执行计划更稳定,不会因复杂合并逻辑导致意外性能退化
内容的提问来源于stack exchange,提问作者Mason Wheeler
相关产品推荐
相关产品推荐

