You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.02 00:03:10