GROUP BY慢的关键在于未走索引导致Using temporary和Using filesort;应建WHERE等值列+GROUP BY列+SELECT字段的最左前缀覆盖索引,并避免函数、隐式转换等使索引失效的操作。

GROUP BY 为什么慢?关键在临时表和排序
分组查询变慢,通常不是因为 GROUP BY 本身复杂,而是执行过程中被迫创建磁盘临时表、触发 Using temporary; Using filesort。MySQL 或 PostgreSQL 在无法利用索引顺序完成分组时,会把中间结果写入 tmp_table(内存不足就落盘),这在千万级数据上直接拉垮性能。
常见现象包括:
- EXPLAIN 中
Extra字段出现Using temporary; Using filesort - 查询响应时间随数据量非线性增长(比如从 10 万到 100 万行,耗时翻 5 倍以上)
-
tmp_table_size和max_heap_table_size频繁被突破,Created_tmp_disk_tables指标飙升
覆盖索引怎么建?顺序比列数更重要
覆盖索引对 GROUP BY 的价值,是让分组字段天然有序 + 所有需要的字段都在索引里,避免回表和额外排序。但错的顺序等于没建。
正确建法必须按三段式排列:
-
WHERE 等值条件列放最左(如
status = 'paid') -
GROUP BY 列紧随其后(如
dept_id),保证过滤后数据已按分组字段有序 -
SELECT 中的聚合依赖字段放最后(如
amount,用于SUM(amount)),形成覆盖
例如这个查询:SELECT dept_id, COUNT(*), SUM(amount) FROM orders WHERE status = 'paid' GROUP BY dept_id,对应索引应为:INDEX idx_status_dept_amount (status, dept_id, amount)。注意:不能写成 (dept_id, status, amount) —— 这样 WHERE 就用不上索引了。
哪些操作会让覆盖索引失效?
哪怕索引建得再准,只要查询里出现这几类写法,优化器就会放弃使用它:
- 在
GROUP BY或WHERE中对字段用函数,如GROUP BY YEAR(created_at)、WHERE UPPER(name) = 'ADMIN' - 隐式类型转换,如
WHERE user_id = '123'(user_id是INT) -
SELECT *—— 主键以外的字段大概率不在索引里,必然回表 - 用
LIKE '%abc'开头的模糊匹配,破坏最左前缀
验证是否真用了覆盖索引,只看 EXPLAIN 的 Extra 字段:Using index 表示成功;Using where; Using index 也算;但凡带 Using temporary 或 Using filesort,就说明索引没起作用。
高基数分组字段要特别小心
对 UUID、手机号、长文本字段做 GROUP BY,极易触发磁盘临时表——不是因为没索引,而是每个分组值都太“散”,内存装不下。这时加索引效果有限,优先考虑逻辑降维:
- 改用哈希前缀分组,如
GROUP BY LEFT(phone, 5)(需确保业务可接受) - 预聚合:建一张每日/每小时汇总表,把
GROUP BY搬到写入链路里,查时直接SELECT * FROM daily_dept_summary - 限制返回数量,配合
LIMIT+ 子查询,如先SELECT dept_id FROM orders WHERE ... GROUP BY dept_id ORDER BY COUNT(*) DESC LIMIT 10,再关联取详情
真正难的不是建索引,是判断什么时候不该用 GROUP BY 实时算——尤其当分组维度多、基数高、并发高时,物化视图或定时汇总往往比“优化 SQL”更治本。



















