导读:本期聚焦于小伙伴创作的《SQL数据库聚合执行路径:单阶段与多阶段聚合有什么区别?》,敬请观看详情。把SUM或COUNT这类聚合直接放在数据扫描后一步完成,和先局部聚合再全局汇总,两种执行路径在分布式环境里性能差异极大。单阶段聚合逻辑简单,但网络传输原始行会带来巨大开销;多阶段聚合通过引入中间合并层,显著削减shuffle数据量。不少查询变慢的根源,正是优化器误选了路径。理清二者在算子调度、内存占用与并行度上的不同,才能针对大表分组统计写出更稳的SQL,也更容易看懂执行计划里的HashAgg与MergeAgg节点。

在SQL查询引擎处理GROUP BY配合SUM、COUNT、MAX等聚合函数时,底层存在两种典型的执行路径:单阶段聚合与多阶段聚合。理解它们如何调度算子、怎样搬运数据,是分析慢查询和阅读执行计划的基础。

SQL数据库聚合执行路径:单阶段与多阶段聚合有什么区别?

一、单阶段聚合的执行机制

单阶段聚合是最直观的实现方式。执行引擎在扫描完数据或者接收完上游输入后,直接在一个算子内维护聚合状态,并输出最终分组结果。以PostgreSQL风格的HashAggregate为例,它在内存中建立哈希表,键为分组列,值为累加状态,每来一行就更新对应条目,扫描结束即得到结果。

这种路径的优点是逻辑简单、延迟低,没有额外的数据重分布。但当查询涉及分布式节点时,如果先在各个分片本地扫描,再必须把全部原始明细发往一个汇总节点做聚合,网络与单点内存压力就会非常突出。下面的代码展示了一个简单单阶段聚合的伪逻辑:

// 单阶段聚合:接收所有行,本地一次性聚合
Map<String, Long> aggMap = new HashMap<>();
for (Row row : allRowsFromNetwork) {
    String groupKey = row.getGroup();
    long val = row.getValue();
    aggMap.put(groupKey, aggMap.getOrDefault(groupKey, 0L) + val);
}
// 直接输出 aggMap 作为最终结果

从优缺点来看,单阶段聚合在单机小数据量时效率很高,但放到集群环境,若优化器没有做本地预聚合,就会出现“全量shuffle”。此时单个汇总节点容易成为瓶颈,且内存放不下时会落盘甚至报错。

二、多阶段聚合的执行机制

多阶段聚合通常分为本地聚合(Partial Agg)和全局聚合(Final Agg)两步。各个计算节点先对本地数据进行第一轮哈希聚合,大幅压缩行数,然后把缩小后的中间结果按分组键重分布到汇总节点,再做第二轮合并。Spark、Flink以及主流MPP数据库都采用这种思路。

在执行计划里,你常会看到类似HashAgg(局部)和MergeAgg或Final HashAgg的节点。局部阶段输出的是“部分求和”而非明细,例如三台机器分别对各自分片算出每个组的SUM,再发给汇总机相加。代码示例如下:

# 阶段一:本地预聚合
local_map = {}
for row in local_scan():
    k = row.group_col
    local_map[k] = local_map.get(k, 0) + row.val_col

# 按分组键 shuffle 到不同节点
send_to_partition(local_map)

# 阶段二:全局合并
global_map = {}
for partial in receive_partitions():
    for k, v in partial.items():
        global_map[k] = global_map.get(k, 0) + v

多阶段聚合显著降低了网络传输量,也避免了单点处理全部明细。代价是引入了额外的算子调度与一次数据重分布,当分组基数极低(例如只有个位数分组)时,本地聚合收益有限,多一轮调度反而略微增加开销。

三、执行路径的选择与优化器决策

数据库优化器会基于统计信息决定走单阶段还是多阶段。如果表很小、或分组键基数极小,优化器可能认为直接单阶段更划算;若表大且分布跨节点,通常插入本地预聚合。但统计信息不准时,就会选错路径,造成性能悬崖。

作为开发者,可以通过查看EXPLAIN观察是否存在“Partial”或“Local”聚合节点。如果大表聚合却只见单一天花板节点,可考虑改写SQL、加分布式 hint,或调大本地聚合内存。以下SQL用于观察计划:

EXPLAIN ANALYZE
SELECT user_id, COUNT(*) AS cnt
FROM orders
GROUP BY user_id;

表中常见算子含义可用下表对照:

计划节点所属阶段作用
HashAggregate可能单阶段或局部基于哈希表做分组累加
MergeAppend / Final Agg全局阶段合并各节点部分结果
Exchange中间重分布按分组键网络重发

掌握这些差异后,在写跨节点大表报表SQL时,应尽量避免在聚合前做不必要的列展开,保证优化器能安全下推局部聚合,从而稳定利用多阶段路径。

四、实践中的避坑建议

一个常见误区是认为“GROUP BY一定会多阶段聚合”。实际上有些函数在语义上不可局部合并,比如求中位数、数组去重计数近似场景,引擎可能退化为单阶段搬运明细。此时需要确认函数是否支持partial聚合。

另外,在UDF中写自定义聚合时,若未实现merge接口,框架也只能走单阶段。下面伪代码展示了一个正确支持多阶段的UDF结构:

public class MySum implements Aggregator {
    // 局部累加
    public State add(State s, long v) {
        s.sum += v;
        return s;
    }
    // 全局合并两个局部状态
    public State merge(State s1, State s2) {
        s1.sum += s2.sum;
        return s1;
    }
}

总之,单阶段与多阶段聚合并非简单优劣关系,而是执行路径在资源与传输之间的权衡。结合执行计划与实际数据分布来审视,才能写出高效的聚合查询。

SQL聚合执行路径单阶段_多阶段修改时间:2026-08-05 19:03:31

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