聚合OOM主因是中间状态过大和单点倾斜;GROUP BY/COUNT(DISTINCT)将分组键与聚合值缓存在内存哈希表,分组数超千万或某组数据量极大(如user_id对应500万日志)即触发溢出。

聚合查询内存溢出,不是加内存就能解决的——核心是控制中间状态大小和避免单点倾斜。
为什么GROUP BY或COUNT(DISTINCT)容易OOM
数据库执行聚合时,会把匹配行的分组键和聚合值(如sum、count)缓存在内存哈希表里。一旦分组数超千万、或某一分组数据量极大(比如某个user_id对应500万条日志),哈希表就撑爆JVM堆或数据库工作内存。
- MySQL 的
sort_buffer_size和read_rnd_buffer_size对 GROUP BY 影响有限,它主要管排序,不直接控聚合内存 - PostgreSQL 的
work_mem才真正决定哈希聚合能用多少内存;设太大反而导致并发查询争抢,多个查询一起触发OOM - Elasticsearch 的
aggs默认在 heap 内构建桶,size超 10000 或未设terminate_after容易夯住节点
用主键分片+临时表替代全量聚合
不依赖数据库自动优化,而是把大聚合拆成可控小任务。关键前提是左表有单调主键(如 id 或 create_time),且右表关联字段有索引。
- 先查主键范围:
SELECT id FROM orders WHERE create_time >= '2026-04-01' AND id BETWEEN 100000 AND 200000; - 把这批 id 插入临时表:
CREATE TEMPORARY TABLE tmp_order_ids (id BIGINT PRIMARY KEY); INSERT INTO tmp_order_ids VALUES (100001), ..., (200000); - 再聚合:
SELECT user_id, COUNT(*) FROM orders JOIN tmp_order_ids USING (id) GROUP BY user_id; - 临时表走索引 + 小批次,每次聚合只处理几万行,内存峰值稳定在 50–200MB 区间
MySQL/PG中开启游标流式读取
客户端一次性拉全量结果集(尤其是带 COUNT(DISTINCT) 或多层嵌套聚合)会把全部中间结果缓存在 JDBC 驱动内存里,不是数据库端的问题,而是客户端崩了。
- MySQL 连接串必须加:
?useCursorFetch=true&defaultFetchSize=500,否则驱动默认把整个结果集 load 到内存 - PostgreSQL 要设
fetchSize=500并显式启用游标:statement.setFetchSize(500); ResultSet rs = statement.executeQuery("SELECT ... GROUP BY ..."); - 注意:
defaultFetchSize只对 SELECT 有效,对 INSERT … SELECT 或 CTE 聚合无效
容易被忽略的三个硬限制
调参前先确认这些底层约束,否则白改配置:
- MySQL 8.0+ 的
CTE聚合结果默认不可下推物化,WITH t AS (SELECT ...) SELECT COUNT(*) FROM t GROUP BY x仍可能 OOM,得加MATERIALIZED提示 - PostgreSQL 的
HASHAGG在work_mem不足时会 fallback 到SORTAGG,但 SORTAGG 仍需内存排序,不是自动落盘 - Elasticsearch 的
composite聚合虽支持分页,但每个after请求仍要重建部分桶结构,高频翻页照样吃内存,得配合size: 1000+timeout

















