GROUP BY内存溢出主因是并发查询叠加申请哈希内存超限,需通过执行计划查真实用量、限制单会话参数、强制SortAggregate、大字段哈希化及避免SELECT *来根治。

GROUP BY 触发全局内存溢出,根本不是单个查询“太贪吃”,而是多个并发查询各自申请哈希内存(如 PostgreSQL 的 work_mem、MySQL 的 tmp_table_size),叠加后远超物理内存上限——调参不控并发,等于给火药桶装压力表。
查清每个查询实际占多少内存,别猜
数据库不会主动告诉你“这个 GROUP BY 吃了 1.2GB”,得自己挖执行计划里的真实用量:
- PostgreSQL:运行
EXPLAIN (ANALYZE, BUFFERS),重点看Hash Agg节点是否带Spill to disk;再查pg_stat_statements里该查询的total_exec_time和blk_read_time,磁盘读飙升就是内存早撑不住了 - MySQL:执行
EXPLAIN FORMAT=TRADITIONAL,只要Extra列出现Using temporary,就说明已放弃索引分组、全量加载进哈希表——此时tmp_table_size再大也救不了,因为瓶颈在算法,不在阈值 - SQL Server:打开
SET STATISTICS XML ON,在 XML 执行计划里搜GrantedMemory_KB和RequiredMemory_KB;如果后者比前者大 30% 以上,且grant_time为空,说明正卡在内存排队队列里
别全局改 work_mem 或 tmp_table_size
设高了不是提速,是制造雪崩:
- PostgreSQL 的
work_mem = '512MB'是每个操作符独占的——一个查询含JOIN + GROUP BY + ORDER BY,就默认吃掉 1.5GB;50 个并发直接干爆 64GB 内存 - MySQL 的
tmp_table_size和max_heap_table_size必须设为完全相等值,否则取小者生效;设成1G却漏配max_heap_table_size,实际还是按默认 16MB 跑 - 临时调参只对当前会话有效:
SET LOCAL work_mem = '128MB'(PG)或SET SESSION tmp_table_size = 536870912(MySQL)——上线前必须压测验证并发承载力
强制走 SortAggregate,绕开哈希表膨胀
哈希表内存 = 分组数 ×(键体积 + 聚合值体积),而排序聚合是流式处理,内存峰值只取决于排序缓冲区大小,与分组数无关:
- 加
ORDER BY且顺序、方向必须和GROUP BY完全一致,例如GROUP BY user_id, status ORDER BY user_id, status;有匹配联合索引时,MySQL 可直接用Using index for group-by流式聚合 - 避免在
GROUP BY或ORDER BY中混用函数:GROUP BY DATE(created_at)或ORDER BY UPPER(name)会让索引失效,退化回哈希 - 索引顺序必须严格对齐:想用
INDEX(status, created_at)加速GROUP BY status, created_at ORDER BY status, created_at,反过来建INDEX(created_at, status)就无效
大字段必须哈希化,否则单键就能撑爆内存
一个 VARCHAR(2000) 字段,平均长度 800 字节,分组键数 1000 万 → 光键就占 8GB 内存,还没算聚合值。这不是参数能扛的:
- 改用确定性哈希:
GROUP BY SHA2(email, 256)(MySQL)或GROUP BY md5(email)(PostgreSQL),并建函数索引:CREATE INDEX idx_email_hash ON t (SHA2(email, 256)) - JSON 字段提取关键路径再哈希:
GROUP BY SHA2(JSON_EXTRACT(data, '$.user_id'), 256),避免整字段参与哈希 - 绝对禁用
LEFT(long_text, 100)这类无索引表达式——它既不减内存,又让所有索引失效;SUBSTRING_INDEX(url, '/', 3)可行,但需确认数据库版本支持函数索引(MySQL 8.0+ / PG 12+)
最容易被忽略的是:GROUP BY 大字段 + SELECT * + WHERE 条件没走索引,三者叠加会让内存占用呈指数级增长——哈希表里存的不是分组键,而是整行数据的副本。先砍字段,再建索引,最后才考虑调参。

















