group by 慢是因为未走索引导致触发 using temporary;需建联合索引,将 where 等值字段前置、group by 字段紧随其后,并覆盖 select 字段,使 mysql 能顺序扫描天然分组。

GROUP BY 为什么慢?先看执行计划里的 Using temporary
MySQL 执行 GROUP BY 时,若没有合适索引,会强制创建临时表来去重分组——这在 EXPLAIN 的 Extra 列里显示为 Using temporary。只要看到它,基本就说明查询正在走全表扫描 + 内存/磁盘临时表路径,数据量一过百万,性能断崖式下跌。
常见诱因包括:
-
GROUP BY字段无索引,或索引未覆盖查询条件(比如WHERE user_id = ?和GROUP BY viewed_user_age用的是两个不相干的单列索引) - 索引顺序与分组字段顺序不一致(如索引是
(c1, c2, c3),但GROUP BY c2, c1) - SELECT 中包含非分组字段且未在索引中(触发回表,甚至让索引失效)
建什么索引才能让 GROUP BY 走索引分组?
核心原则:让 MySQL 能“顺着索引顺序读,相同值天然连续”,从而跳过临时表。这要求索引列顺序严格匹配 GROUP BY 字段顺序,并尽可能包含 WHERE 条件字段和聚合需要的字段(即覆盖索引)。
以这个典型慢查为例:
SELECT viewed_user_age, COUNT(*) FROM t_user_view WHERE user_id = 1 AND viewed_user_sex = 1 GROUP BY viewed_user_age;
最优索引应为:
KEY `idx_user_sex_age` (`user_id`, `viewed_user_sex`, `viewed_user_age`)
理由:
-
user_id和viewed_user_sex是等值过滤条件,放最左,缩小扫描范围 -
viewed_user_age紧跟其后,满足分组字段顺序,MySQL 可对每个(user_id, viewed_user_sex)组内的viewed_user_age值直接顺序扫描、分组 - 该索引已覆盖全部查询字段,无需回表,
EXPLAIN中type会是ref,Extra不再有Using temporary或Using filesort
GROUP BY 后带 ORDER BY,索引怎么设计?
MySQL 默认会对 GROUP BY 结果按分组字段升序排序。如果你额外写 ORDER BY viewed_user_age DESC,而索引是升序的,就会触发 Using filesort —— 即便字段一样。
解决方式分两种场景:
- 不需要排序结果:直接加
ORDER BY NULL,显式告诉 MySQL 别费劲排序,能省掉一次排序开销 - 必须按特定方向排序(如
DESC):MySQL 8.0+ 支持降序索引,可建KEY idx_user_sex_age_desc (user_id, viewed_user_sex, viewed_user_age DESC);5.7 及更早版本只能靠调整查询逻辑或接受Using filesort - 如果
ORDER BY字段和GROUP BY不同(如GROUP BY age但ORDER BY COUNT(*) DESC),索引无法避免排序,此时优先保证分组走索引,排序靠sort_buffer_size参数或应用层处理
为什么 ANALYZE TABLE 很关键?
即使建了正确索引,如果表统计信息陈旧,MySQL 优化器可能误判成本,仍选择全表扫描。尤其在大量 INSERT/DELETE 后,ANALYZE TABLE t_user_view 是低成本高回报的操作。
注意点:
- 不要依赖自动统计(
innodb_stats_auto_recalc默认开启但有延迟),大表变更后手动执行更稳妥 -
ANALYZE TABLE是轻量级操作,不会锁表,但会短暂阻塞 DML(实际影响极小) - 验证是否生效:执行
EXPLAIN后对比key和rows是否明显下降
真正难的不是建索引,而是确认 WHERE 条件、GROUP BY 字段、SELECT 字段三者能否被同一个索引“一气呵成”地服务。漏掉一个字段,或者顺序错一位,就可能退回临时表老路。每次加索引前,务必用 EXPLAIN 对照着看。











