根本原因是GROUP BY字段无合适BTREE索引,导致MySQL无法直接按序遍历分组,必须创建临时表并排序;常见触发点包括分组列缺失索引、索引非BTREE类型、WHERE含范围条件使索引无法覆盖分组顺序。
根本原因不是 GROUP BY 本身慢,而是 MySQL 没法靠索引直接完成分组,只能先把数据捞出来塞进临时表再算。常见触发点有:
• GROUP BY 字段没索引,或索引不是 BTREE 类型(比如 HASH 索引)
• WHERE 条件用了范围查询(如 id > 100 AND id ),而 <code>GROUP BY 字段不在该索引最左前缀位置
• SELECT 列里混了非分组、非聚合字段(比如 SELECT name, COUNT(*) FROM t GROUP BY dept_id),MySQL 5.7+ 默认报错或强制建临时表
• ORDER 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';
• 如果分组结果集预估行数 × 平均行宽 • 对超大分组(比如千万级唯一 user_id),反而加 SQL_BIG_RESULT 提示:强制走磁盘临时表 + 外部排序,避免反复内存分配失败
• sort_buffer_size 也得跟上,否则 Using filesort 会卡住整个流程
业务常要“每个分组里取最新一条的 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),但务必给关联字段建索引,否则更慢