执行计划中出现hash match(aggregate)或hashagg即表示启用哈希聚合,它不依赖输入顺序,通过内存哈希表实现o(n)分组聚合,但高基数分组易引发磁盘溢出。

哈希聚合在执行计划里长什么样
看到执行计划里出现 Hash Match(Aggregate)(SQL Server)或 HashAgg(PolarDB-X、PostgreSQL 部分版本)就说明引擎选了哈希聚合。它不依赖输入顺序,也不需要提前排序,但会在内存中建一张哈希表,键是 GROUP BY 列的值,值是该组的聚合中间状态(比如当前计数、累加和等)。
常见触发场景:
- 没给
GROUP BY字段建索引,或索引不满足最左前缀(如建了(a, b)却按b分组) -
ORDER BY和GROUP BY字段不一致,导致无法复用排序结果 - 查询带
JOIN且连接后数据无序,优化器判断排序代价高于哈希开销
哈希聚合为什么比排序分组快,又为什么容易爆内存
哈希聚合平均时间复杂度是 O(n),扫描一遍数据就能完成分组+聚合;排序分组要先花 O(n log n) 排序,再线性扫描归并——大数据量时差距明显。
但它吃内存:每个分组都要在哈希表里占一个桶,如果分组数多(比如按高基数列如 user_id 分组),或单个分组数据特别大(比如某用户有百万条记录),哈希表就可能撑满授予内存,触发溢出到磁盘 workfile。这时性能断崖下跌,IO 成瓶颈。
关键控制点:
- SQL Server 中可通过
MAXDOP和查询资源调控器限制内存授予 - MySQL 8.0+ 可调
tmp_table_size和max_heap_table_size影响内部临时表上限 - PolarDB-X 支持 Hint:
/*+TDDL:cmd_extra(ENABLE_HASH_AGG=false)*/强制走SortAgg
怎么让哈希聚合不退化成磁盘溢出
核心思路是减少哈希表压力:要么降低分组数量,要么缩小单组体积。
实操建议:
- 加
WHERE过滤掉无效数据再分组,比如WHERE status = 'done',别让百万草稿记录进聚合 - 避免用高基数列单独分组,可先降维:比如把
user_id替换为user_region或加时间窗口(DATE(created_at)) - 检查
GROUP BY列是否有大量NULL—— 它们会被聚成同一组,容易撑爆桶;必要时用COALESCE(col, 'unknown')拆开 - 确认统计字段类型:用
INT而非VARCHAR(255)做分组键,哈希计算和比较都更快
哈希聚合和索引到底什么关系
索引对哈希聚合**没直接加速作用**——它不靠索引定位,而是全量扫描后哈希散列。但索引会影响优化器决策:如果存在匹配的索引(比如 GROUP BY a, b 且有 (a, b) 联合索引),优化器更倾向走流聚合(Stream Aggregate),因为索引已排序,省去哈希开销。
所以不是“建了索引哈希就快”,而是“建了合适索引,数据库可能干脆不用哈希”。验证方法很简单:在语句末尾加 ORDER BY a, b,如果执行计划从 HashAgg 变成 SortAgg 或直接消失(被索引覆盖),就说明索引生效了。
注意陷阱:
- MySQL/PostgreSQL 要求索引顺序与
GROUP BY字段**严格一致**,GROUP BY b, a无法利用(a, b)索引 - SQL Server 稍宽松,但乱序仍大概率退化为哈希+排序
- 覆盖索引(含所有 SELECT + GROUP BY 字段)能让聚合完全在索引页完成,连表都不用扫










