group by大字段直接撑爆哈希表内存,因数据库原样缓存分组键完整值(如varchar(2000)、text),单键几kb×千万级分组即达数十gb;调大tmp_table_size或work_mem仅延缓崩溃,根本解法是用sha2/md5哈希或substring_index等确定性截断替代原始大字段,并避免select*和where索引失效。

GROUP BY大字段直接撑爆哈希表内存
不是“慢”,是根本跑不起来——MySQL 或 PostgreSQL 在执行 GROUP BY 时,会把**分组键的原始值**(比如 VARCHAR(2000)、TEXT、JSON 字段)完整加载进内存哈希表,每个不同值都占几 KB。一千万个唯一值,光键就吃掉几十 GB 内存,还没开始算 COUNT(*) 就 OOM 了。
关键点在于:数据库不会自动截断、压缩或哈希化这个字段;哪怕你只 SELECT COUNT(*),只要 GROUP BY big_text_column,它就得原样存。
常见错误现象:
-
EXPLAIN显示Using temporary,但没报错——其实是内存快耗尽前的“最后喘息” - 查询卡住十几秒后突然返回
ERROR 1038 (HY001): Out of memory或ERROR: out of memory - 调大
tmp_table_size或work_mem后能跑通小数据,一上生产就崩
为什么加内存参数救不了根本问题
tmp_table_size(MySQL)和 work_mem(PostgreSQL)只是“临界阈值”,不是解决方案。它们控制的是“多大才落磁盘”,而不是“要不要存全量键值”。
真正的问题公式是:哈希表内存 = 分组数 ×(分组键字节数 + 聚合中间值字节数)。字段越大,单桶越重,阈值还没触达,物理内存已满。
容易被忽略的细节:
- MySQL 中
tmp_table_size和max_heap_table_size取较小值生效,设了 1GB 但另一项是 64MB,实际仍按 64MB 限制 - PostgreSQL 的
work_mem = '512MB'是单查询上限,高并发下多个查询争抢,反而触发整体 OOM - 即使 fallback 到磁盘(如 PG 的
SORTAGG),排序阶段仍需把全部分组键加载进内存——不是“自动流式处理”
用确定性哈希替代原始大字段
可行解法只有一个方向:让分组键变小、稳定、可索引。不能靠硬扛内存,得从源头压缩语义。
实操建议:
- 对纯文本字段,改用
GROUP BY SHA2(big_text, 256)(MySQL)或GROUP BY MD5(big_text)(兼容性更好),固定 32/64 字节,冲突概率极低 - 对 JSON 字段,先提取关键路径再哈希:
GROUP BY SHA2(JSON_EXTRACT(data, '$.user_id'), 256) - 若必须按前缀分组(如 URL 域名),用
GROUP BY SUBSTRING_INDEX(url, '/', 3),但确保该表达式能走函数索引(MySQL 8.0+ / PG 12+) - 绝对避免
GROUP BY LEFT(long_text, 100)——既不减内存占用,又让索引失效,还无法复用
隐性放大器:SELECT * 和 WHERE 条件设计不当
你以为只改 GROUP BY 就够?错。这两处会指数级放大哈希表体积:
-
SELECT *会让 MySQL 回表取全行,再把整行塞进临时表,哪怕你只按一个字段分组 -
WHERE条件含范围查询(如created_at > '2024-01-01')且放在联合索引中间位置,会截断索引有效长度,导致无法有序扫描,被迫建更大临时表 - 隐式类型转换(如
user_id = '123'对比BIGINT字段)会让索引失效,退化为全表扫描,分组前就要加载千万行原始数据
最常被忽略的一点:即使你给大字段单独建了索引,只要 WHERE 或 SELECT 涉及其他列,优化器大概率不会用它——必须建覆盖完整访问链的联合索引,否则哈希表照旧膨胀。










