GROUP BY触发全表扫描+HASH聚合导致CPU飙升,根本原因是WHERE未走索引或缺失,致使数据库需加载全部数据进内存分组;应确保WHERE条件命中索引、GROUP BY字段顺序与联合索引严格一致、避免函数/类型转换,并控制SELECT字段减少回表开销。

GROUP BY 触发全表扫描 + HASH 聚合,CPU 直接飙高
不是 GROUP BY 本身慢,而是它前面没拦住数据——如果 WHERE 条件没走索引、或根本没加 WHERE,MySQL/PostgreSQL 就得先把几百万行全读进内存,再逐行哈希分桶、计算聚合值。这个过程几乎纯靠 CPU,不怎么依赖磁盘,所以 top 看到的是 sqlservr 或 mysqld 进程占满 CPU,而不是 IO 等待。
常见错误现象:
-
EXPLAIN显示type: ALL或rows值远超预期(比如表有 500 万行,rows显示 480 万) - 执行计划里出现
HashAggregate或HASH GROUP BY,且Batches > 1(说明内存不够,开始写磁盘临时文件) - MySQL 的
SHOW PROCESSLIST中看到状态为Creating sort index或Copying to tmp table
GROUP BY 字段没索引,但你以为“有主键就够了”
主键索引只对 GROUP BY primary_key 有效;一旦是 GROUP BY status, created_at 或 GROUP BY YEAR(created_at),主键就完全失效。优化器没法用主键顺序做流式聚合(Stream Aggregate),只能退回到代价更高的 HASH 或 SORT 方式。
实操建议:
- 联合索引必须严格匹配
GROUP BY字段顺序:GROUP BY a, b→ 建(a, b),不是(b, a) - 避免在分组字段上用函数:
GROUP BY DATE(created_at)会放弃所有索引;改用生成列:ALTER TABLE t ADD COLUMN created_date DATE AS (DATE(created_at)) STORED,再建索引(created_date) - 确认是否真需要实时分组:大表按天汇总订单,优先考虑物化视图或定时写入汇总表,而非每次查都
GROUP BY DATE(created_at)
SELECT * + GROUP BY = 双倍 CPU 开销
SELECT * 强制回表取所有字段,而 GROUP BY 过程中每分一组都要反复读取这些冗余字段——尤其当表里有 TEXT、JSON 或长 VARCHAR 时,CPU 要花大量时间拷贝、比较、哈希这些大块内存。更糟的是,这会让原本能走覆盖索引的查询彻底失效。
关键参数影响:
- MySQL 的
tmp_table_size和max_heap_table_size默认 32MB,一旦 GROUP BY 中间结果超限,立刻落盘,触发磁盘排序,CPU 持续满载还带高 IO - PostgreSQL 的
work_mem控制单个排序/哈希操作内存上限,设太小(如 4MB)会导致external merge Disk,性能断崖下跌 - 别全局调大这些值——并发一高就 OOM;应定位到具体慢 SQL,用
SET LOCAL work_mem = '64MB'或加/*+ SET_VAR(tmp_table_size = 67108864) */提示优化器
隐式类型转换让索引彻底失效
比如 user_id 是 BIGINT,但查询写成 WHERE user_id = '123',MySQL 会把所有 user_id 转成字符串再比对,索引直接跳过。更隐蔽的是 GROUP BY 场景:GROUP BY CONCAT('U', user_id) 或 GROUP BY CAST(user_id AS CHAR),同样导致无法使用 user_id 索引,被迫全表扫。
检查方法:
- 用
EXPLAIN FORMAT=JSON查key字段是否为null,同时看filtered值是否异常低(如 10.0,说明实际只过滤掉 90% 数据,剩下 90% 还得参与分组) - 抓出慢日志中的 SQL,用
SELECT @@sql_mode确认是否启用了STRICT_TRANS_TABLES,避免隐式转换被静默吞掉 - 对业务代码中拼接的动态条件做审计:所有传入
GROUP BY的字段,必须和表定义类型一致,宁可显式CAST也不依赖自动转换
真正卡住 GROUP BY 性能的,往往不是聚合逻辑本身,而是分组前的数据输入失控——没 WHERE 过滤、没索引支撑、字段类型不匹配、还顺手 SELECT *。这些问题叠加后,CPU 不是“被用满”,而是“被逼疯”。


















