GROUP BY 查询慢的根本原因,通常并不在于语法本身,而是索引设计中的常见细节错位——分组字段缺少合适的 BTREE 索引,导致 MySQL 只能先创建临时表再进行排序,性能自然大打折扣。常见的触发场景包括:分组列完全没有索引;索引类型非 BTREE(如 HASH);或者 WHERE 条件中包含了范围查询,使得索引无法覆盖分组顺序。

GROUP BY 查询为何总是触发 Using temporary?
实际上,GROUP BY 查询慢的本质原因,并非语法本身效率低,而是 MySQL 无法通过索引直接完成分组操作——它必须先将数据读取到临时表中再进行计算。常见的触发条件包括:
• GROUP BY 字段未建立索引,或索引类型不是 BTREE(例如 HASH 索引),则无法利用索引进行分组。
• WHERE 条件中使用了范围查询(如 id > 100 AND id < 200),并且 GROUP BY 字段不在该索引的最左前缀位置上,导致索引顺序与分组顺序不匹配。
• SELECT 列表中包含了非分组字段且非聚合函数(例如 SELECT name, COUNT(*) FROM t GROUP BY dept_id),MySQL 5.7 及以上版本会直接报错或强制创建临时表来容纳这些“多余”字段。
• ORDER BY 字段与 GROUP BY 字段不一致,且没有公共索引覆盖,也会导致临时表排序。
如何让 GROUP BY 查询跳过临时表?
核心思路是让 MySQL 采用松散索引扫描(Loose Index Scan)——只扫描索引中的不同值,跳过全量行扫描。实际操作中,以下几个要点至关重要:
• 索引必须为 BTREE 类型,且 GROUP BY 字段必须是联合索引的最左前缀。例如,索引 (user_id, created_at) 可以支持 GROUP BY user_id,但无法支持 GROUP BY created_at,因为顺序不匹配。
• 如果存在 WHERE 过滤条件,应将区分度高的过滤字段放在索引最左侧。例如,WHERE status = 'paid' GROUP BY user_id,索引应设计为 (status, user_id),使过滤和分组共享同一索引路径。
• 避免在 GROUP BY 列上使用函数操作,如 GROUP BY YEAR(create_time) 会导致索引失效,无法优化。
• 使用 EXPLAIN 查看 Extra 字段:若出现 Using index for group-by 则表示成功启用松散扫描;若看到 Using temporary,则说明未走通。
临时表已经产生,如何避免写入磁盘?
内存临时表一旦超出容量限制,就会写入磁盘,性能急剧下降。关键参数为两个值中的较小者:tmp_table_size 和 max_heap_table_size,默认值仅为 16MB。优化措施如下:
• 首先查询当前设置:SHOW VARIABLES LIKE 'tmp_table_size'; 和 SHOW VARIABLES LIKE 'max_heap_table_size';,了解上限情况。
• 如果分组结果集的行数乘以平均行宽远小于这两个参数,则直接调大它们,例如 128MB 或 256MB,尽量让临时表保留在内存中。
• 如果遇到超大分组(例如千万级唯一 user_id),则可以考虑添加 SQL_BIG_RESULT 提示,强制使用磁盘临时表和外部排序,避免因内存反复分配失败导致的性能抖动。
• 同时不要忽略 sort_buffer_size 的调整,否则 Using filesort 仍会阻塞整个流程。
SELECT 中包含非分组字段时应如何处理?
业务场景中常需要获取每个分组中最新的一条记录(如 title),但直接编写 SELECT title, COUNT(*) FROM t GROUP BY user_id 会触发 ONLY_FULL_GROUP_BY 错误或临时表。正确的做法是:
• 如果需求是“任意一条即可”,可使用 ANY_VALUE(title),并尽量让 title 字段也在索引中(例如 (user_id, title) 覆盖索引),以避免回表查询。
• 如果需要获取最新或最旧的某个字段,不应强行使用 GROUP BY,而应改用窗口函数:ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY create_time DESC),然后在外层查询中过滤 WHERE rn = 1,既清晰又高效。
• 在极端情况下(如 MySQL 5.6 不支持窗口函数),可采用关联子查询或 JOIN + MAX(id) 的方式,但务必为关联字段添加索引,否则性能可能比 GROUP BY 更差。
总而言之,真正影响性能的,往往不是 GROUP BY 语法本身,而是索引中字段的顺序与 WHERE 条件之间的错位——一个字段位置不当,整个执行计划就会退化为全表扫描加临时表。
