group by直接触发oom是因为数据库将分组键完整值与聚合中间状态全量缓存在内存哈希表中,单键几kb×千万级分组即达数十gb,超限即崩溃而非落盘;postgresql的work_mem和mysql的tmp_table_size是硬上限,不支持优雅降级。

为什么GROUP BY会直接触发OOM,而不是先报错或落盘
数据库执行GROUP BY时,并不等价于“先分组再计算”,而是把**每个分组键的完整值 + 所有聚合中间状态**(如COUNT计数器、SUM累加值、COUNT(DISTINCT)哈希集)一起缓存在内存哈希表里。一旦分组键本身是VARCHAR(2000)、JSON或TEXT,单个键占几KB,千万级不同值就轻松吃掉数十GB内存——根本没机会走到“写磁盘”那步。
PostgreSQL 的 work_mem 和 MySQL 的 tmp_table_size 都不是“自动兜底开关”,而是硬性上限:超了就 OOM,不会优雅降级。更关键的是,MySQL 里 sort_buffer_size 完全不管 GROUP BY 内存,调它等于白忙。
怎么快速判断是真缺内存,还是查询写法错了
别急着改配置,先用执行计划定位根因:
- PostgreSQL:运行
EXPLAIN (ANALYZE, BUFFERS),重点看有没有Hash Agg节点,以及它下面是否出现Spill to disk—— 没 spill 却 OOM,大概率是分组键太大或过滤没下推 - MySQL:用
EXPLAIN FORMAT=TRADITIONAL,检查Extra列:Using temporary; Using filesort表示索引完全失效;Using index for group-by才算走对路 - 快速探查分组基数:用
SELECT COUNT(*) FROM (SELECT 1 FROM t GROUP BY a, b) AS _替代SELECT * FROM t GROUP BY a, b,避免客户端也 OOM
真正有效的三类解法,按优先级排序
调参永远是最后一步。90% 的 GROUP BY OOM,靠以下方式就能解决:
-
砍分组键体积:对大字段不用原值分组,改用确定性哈希,例如
GROUP BY SHA2(long_text, 256)(MySQL/PG均支持),或提取关键路径后哈希:GROUP BY SHA2(JSON_EXTRACT(data, '$.user_id'), 256) -
建对联合索引:顺序必须是
WHERE等值字段 → GROUP BY字段 → SELECT中的聚合字段,例如查询SELECT dept_id, COUNT(*) FROM orders WHERE status = 'paid' GROUP BY dept_id,对应索引为INDEX(status, dept_id) -
分片聚合+临时表:对超大表,先按主键范围分批取 ID 到临时表,再
JOIN聚合,每次控制在 5–10 万行内,避免单次加载全量数据
work_mem 和 tmp_table_size 怎么设才不翻车
这两个参数不是“越大越好”,而是极易引发连锁 OOM:
- PostgreSQL:
work_mem是每个操作独占的,一个查询里有多个GROUP BY或嵌套JOIN,就会申请多份。默认4MB太小,但线上建议只用SET LOCAL work_mem = '64MB'会话级生效,别改全局 - MySQL:
tmp_table_size和max_heap_table_size取较小者生效,设成不同值等于白设。建议两者统一为536870912(512MB),并确认SELECT不含*,否则临时表单行体积暴增 - 绝对避开的操作:
SET GLOBAL work_mem = '2GB'或innodb_buffer_pool_size设到系统可用内存的 90% —— 这会让其他线程和 OS 缓存无处可放,OOM Killer 很可能直接杀掉mysqld
最常被忽略的点:分组键本身是否真的需要区分全部字符?比如 URL 分组,GROUP BY SUBSTRING_INDEX(url, '/', 3) 比 GROUP BY url 节省两个数量级内存,且函数索引在 MySQL 8.0+/PG 12+ 上完全可用。










