核心是tmp_table_size与max_heap_table_size必须设为相同值,否则MySQL取较小值导致强制落盘;优先建匹配GROUP BY顺序的联合索引、精简字段、分片查询,而非盲目调参。

GROUP BY大表导致临时表溢出磁盘,核心不是内存不够,而是数据库被迫全量加载数据进内存哈希表——分组键超千万、单组数据达百万级、或索引完全失效时,tmp_table_size再大也救不了。
为什么调大 sort_buffer_size 完全无效?
sort_buffer_size 只控制 ORDER BY 和 DISTINCT 的排序缓冲区,对 GROUP BY 的哈希聚合过程零影响。真正决定内存临时表上限的是 tmp_table_size 与 max_heap_table_size 中的较小值。两者不一致(比如一个设 512M、另一个保持默认 16M),MySQL 就按 16M 跑,立刻落盘。
- 查当前生效值:
SELECT @@tmp_table_size, @@max_heap_table_size; - 必须在
my.cnf的[mysqld]段里显式写两行:tmp_table_size = 256M和max_heap_table_size = 256M - 重启后执行
SHOW VARIABLES LIKE 'tmp_table_size';确认二者数值完全相等 - 别设太高:超过物理内存 20% 且并发高时,可能触发系统级 OOM
EXPLAIN 显示 Using temporary 就该停手
这个提示代表 MySQL 放弃索引分组,转而构建全量内存哈希表——一旦分组数超限或某组数据过大(如 user_id 对应 400 万日志),必然溢出到磁盘。关键看 Extra 列是否出现 Using index for group-by,没有就说明索引没被用于分组逻辑。
- 复合索引顺序必须严格匹配
GROUP BY字段顺序:GROUP BY status, created_at需要INDEX(status, created_at),反过来无效 - WHERE 条件字段必须放索引最左侧,才能提前过滤数据量;
WHERE reg_date >= '2024-01-01' GROUP BY city应建INDEX(reg_date, city),而非INDEX(city, reg_date) - 避免在
GROUP BY中使用函数:GROUP BY DATE(created_at)或GROUP BY UPPER(name)会彻底废掉索引,需改用生成列 + 索引
客户端 fetchSize 再大也压不住服务端 OOM
useCursorFetch=true&defaultFetchSize=500 这类 JDBC 参数只控制客户端一次拉多少行,服务端仍需完整计算所有分组、生成整个结果集——如果 COUNT(DISTINCT) 或多层嵌套聚合本身就在撑爆内存,客户端优化毫无意义。
- 确认问题在服务端:查
SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables';,若该值持续上涨,且与Created_tmp_tables比值 >10%,就是服务端真落盘 - 对超大表,放弃单次聚合:用主键范围分片,先
INSERT INTO tmp_ids SELECT id FROM t WHERE id BETWEEN ? AND ?,再JOIN聚合,每次控制在 5 万行内 - 精简字段:
SELECT *会把 TEXT/BLOB 全拖进临时表,直接触发落盘;只取分组字段和聚合字段 - 高基数字段(如 UUID、email)慎用
GROUP BY;业务允许时优先用APPROX_COUNT_DISTINCT()替代COUNT(DISTINCT)
最容易被忽略的一点:你以为在优化 GROUP BY,其实数据库正在默默把整张表扫进内存哈希表——那个瞬间,tmp_table_size 和 max_heap_table_size 是否相等,比它们具体设成多少更重要。

















