为了让你一眼看透本质,我们把 Spark 聚合 比作"分拣扑克牌":
-
HashAggregate = 按点数直接扔进对应的桶(内存中建哈希表)。
-
SortAggregate = 先把牌排好序,再从头数(排序后顺序扫描)。
假设我们有一堆杂乱的扑克牌(数据),要统计 每种花色(Suit)的张数 (GROUP BY suit)。
原始数据(乱序):
♥️3, ♠️K, ♦️5, ♥️7, ♠️A, ♦️2, ♣️4, ♣️Q
玩法一:HashAggregate(哈希聚合)------ "按花色扔桶"
操作步骤:
-
Spark 在内存中建一个"哈希桶"(HashMap),每个花色对应一个格子。
-
每来一张牌,直接计算花色 Hash 值,找到对应的格子:
-
看到 ♥️3 → 找到"红桃"格,计数 1。
-
看到 ♠️K → 找到"黑桃"格,计数 1。
-
看到 ♦️5 → 找到"方块"格,计数 1。
-
看到 ♥️7 → 找到"红桃"格,计数变成 2(原地更新)。
-
-
所有牌读完,直接统计每个格子里的总数。
| 维度 | 特点 |
|---|---|
| 优点 | 速度极快,数据来一条处理一条,无需等待其他数据。 |
| 缺点 | 吃内存。如果花色(分组 Key)有上亿种,哈希表会撑爆内存,导致 OOM 或频繁溢写磁盘。 |
| 适用场景 | 数据分组后 Key 的种类相对较少,内存装得下。 |
玩法二:SortAggregate(排序聚合)------ "排队后顺序数"
操作步骤:
1. 先全局/分区排序:不管牌原来在哪,先把所有扑克牌按"花色"排好队(Shuffle + Sort)。排完后顺序为:
♣️4, ♣️Q, ♦️2, ♦️5, ♥️3, ♥️7, ♠️A, ♠️K
2. 再顺序扫描:从头到尾扫一遍,因为同花色都挤在一起了,所以只需要记住"当前花色":
- 扫到 ♣️4(当前为梅花),累加;扫到 ♣️Q(还是梅花),累加;遇到 ♦️2(花色变了),立刻把梅花的统计结果输出,然后开始数方块。
- 扫完整个队列,所有统计结果就出来了。
| 维度 | 特点 |
|---|---|
| 优点 | 内存占用小。排序时可以借助磁盘(外部排序),不需要在内存中维持巨大的哈希表。 |
| 缺点 | 速度较慢。必须等所有数据排好序才能开始聚合,且排序本身消耗大量的 CPU 和 Shuffle 网络开销。 |
| 适用场景 | 数据量极大,Key 种类极多,内存不够装下哈希表时,作为"兜底策略"使用。 |
两者终极对决(一图看懂)
| 对比维度 | HashAggregate | SortAggregate |
|---|---|---|
| 核心操作 | 哈希查找 + 内存更新 | 全局排序 + 顺序遍历 |
| 对数据要求 | 不需要数据有序 | 必须先按 Key 排好序 |
| 内存风险 | 高(Key 太多会撑爆) | 低(依赖磁盘缓冲) |
| 执行效率 | 快(O(1) 查找) | 慢(O(n log n) 排序) |
| Spark 优先度 | 默认首选(高性能) | 当 HashAgg 内存不足时回退使用 |
Spark 实际执行中的"骚操作"(必看)
在实际的物理执行计划中,Spark 为了兼顾速度和稳定性,常采用两阶段聚合:
-
部分聚合(Partial HashAggregate) :
在每个数据分片(Task)内部,先用 HashAggregate 快速合并局部数据(比如每个 Task 内先数一遍自己手上的牌),大幅减少 Shuffle 的数据量。
-
最终聚合(Final 阶段) :
Shuffle 后,如果 Key 的数量依然巨大,Spark 可能会放弃 HashAggregate,改用 SortAggregate 来完成最后的全局合并,防止内存爆炸。
在 Spark SQL 中,HashAggregate 是默认首选,SortAggregate 则作为"替补" ,在 HashAggregate 无法胜任或效率不佳时登场。
简单来说,HashAggregate 追求极致速度,而 SortAggregate 保证系统稳定。
🚀 HashAggregate:高性能的默认选项
只要条件允许,Spark SQL 会优先选择 HashAggregate 来追求最佳性能。它通常出现在这些场景:
-
标准 GROUP BY 聚合 :这是最典型的场景,例如
SELECT category, SUM(sales) FROM table GROUP BY category。 -
DISTINCT 去重聚合 :执行
SELECT COUNT(DISTINCT col) FROM table这类操作时,Spark 会使用HashAggregate分阶段去重。 -
无 GROUP BY 的全局聚合 :即使是计算整个表的
SELECT COUNT(*) FROM table,也会使用HashAggregate进行全局聚合。
值得注意的是,HashAggregate 通常以"成对"方式出现:先在每个分区内进行局部聚合 (Partial),经过 Exchange 节点完成数据 shufflle 后,再进行全局聚合(Final)。
🛡️ SortAggregate:保障稳定的后备方案
当出现以下情况时,Spark SQL 会放弃 HashAggregate,转而使用更稳定的 SortAggregate:
-
内存压力过大 :当分组键(Key)过多,导致
HashAggregate使用的哈希表大到无法放入内存时,Spark 会"回退"到SortAggregate以避免内存溢出(OOM)。 -
数据类型不匹配(核心原因) :
HashAggregate要求聚合函数中使用的数据类型是"可变"的。当你使用first()、last()或某些自定义聚合函数(UDAF) 时,会因数据类型限制而强制使用SortAggregate。 -
数据已预先排序 :如果数据流本身已经按分组键排好序,那么使用
SortAggregate可以省去额外的排序开销,速度可能更快。
💎 总结
Spark SQL 选择聚合策略的逻辑是:
-
首选
HashAggregate:利用哈希表进行O(1)查找,实现高性能聚合。 -
条件不满足时,降级为
SortAggregate:通过先排序、后聚合的方式,用更稳定的性能表现换取系统的可靠性。