GROUP BY内存溢出主因是哈希表中间状态超限,非调sort_buffer_size可解;应优先建覆盖索引、精简字段、分片查询,再合理设work_mem或tmp_table_size/max_heap_table_size。

直接结论:GROUP BY 内存溢出不是靠调大 sort_buffer_size 或堆内存能解决的,核心是控制哈希表中间状态大小——分组键数量、单组数据量、聚合字段体积三者叠加超限,就会触发 OOM;响应慢则大概率卡在 Using temporary; Using filesort,本质是没走索引。
为什么EXPLAIN里总出现Using temporary
MySQL 或 PostgreSQL 在无法利用索引顺序完成分组时,会强制建内存临时表(超限就落盘),这是性能塌方和内存爆掉的第一信号。
- 常见诱因:
GROUP BY字段没索引,或索引顺序与查询不匹配(比如索引是(status, user_id),但写成GROUP BY user_id, status) - 函数干扰:写成
GROUP BY DATE(created_at)或SUBSTRING(name, 1, 5),索引直接失效 - SELECT * 拖累:即使分组字段有索引,
SELECT *仍会触发回表+临时表,单行体积翻倍 - WHERE 条件未覆盖:索引建了
(user_id),但查询带WHERE status = 'paid',实际还是全表扫
PostgreSQL 中 work_mem 怎么设才不翻车
work_mem 是每个操作独占的内存上限,不是共享池——一个查询里有 JOIN + GROUP BY + ORDER BY,会各自申请一份,加起来就是三倍消耗。
- 先确认是否真缺内存:运行
EXPLAIN (ANALYZE, BUFFERS),看Hash Agg节点有没有Spill to disk;没有 spill 却 OOM,说明是查询逻辑问题 - 别改全局:用
SET LOCAL work_mem = '128MB'仅对当前会话生效,避免高并发下集体抢内存 - 设太高反而出事:50 个连接 × 256MB = 12.8GB,远超
shared_buffers,可能把整个实例拖垮 - fallback 不等于安全:
work_mem不足时退化为Sort Agg,它仍需内存排序,不是自动落盘
MySQL 下真正起作用的参数和索引写法
MySQL 没有 work_mem,sort_buffer_size 管排序,不管 GROUP BY 内存;真正决定能否走索引分组的是联合索引结构和查询写法。
- 索引必须按三段式建:
WHERE等值字段 →GROUP BY字段 →SELECT中的聚合字段,例如查询SELECT dept_id, COUNT(*), SUM(amount) FROM orders WHERE status = 'paid' GROUP BY dept_id,对应索引是INDEX(status, dept_id, amount) - 查执行计划看关键指标:
Extra列出现Using index for group-by才算成功;若仍是Using temporary; Using filesort,说明索引没命中 - 临时表阈值要同步调:
tmp_table_size和max_heap_table_size必须设为相同值(如536870912),MySQL 取二者较小者生效 - CTE 聚合默认不物化:MySQL 8.0+ 中
WITH t AS (SELECT ...) SELECT COUNT(*) FROM t GROUP BY x仍可能 OOM,得显式加MATERIALIZED提示
比调参更可靠的解法:主键分片 + 临时表
当分组键超千万、或某用户日志达百万级时,任何参数调整都只是延缓爆炸——唯一可控的方式是把“一次大聚合”拆成“多次小聚合”,每次只处理几万行。
- 前提条件:左表有单调主键(如
id或create_time),右表关联字段有索引 - 操作步骤:先
CREATE TEMPORARY TABLE tmp_ids (id BIGINT PRIMARY KEY)插入一批 ID;再SELECT user_id, COUNT(*) FROM log JOIN tmp_ids USING (id) GROUP BY user_id - MySQL 游标流式读取要加参数:连接串必须含
?useCursorFetch=true&defaultFetchSize=500,否则setFetchSize()无效 - PG 显式游标不能少:
DECLARE agg_cursor CURSOR FOR SELECT ... GROUP BY ...,再FETCH 500分批拉,否则 JDBC 仍会一次性加载全部结果集到驱动内存
最容易被忽略的一点:客户端 OOM 和数据库端 OOM 是两回事。JDBC 的 setFetchSize(Integer.MIN_VALUE) 只缓解客户端堆溢出,数据库服务端仍要算完全部分组才开始发数据——如果服务端已经撑不住,调 fetch size 毫无意义。


















