GROUP BY 慢主要因MySQL默认用紧凑索引扫描(遍历所有行),而松散索引扫描(只取每组首行)需满足严格条件:GROUP BY列为索引最左前缀、无范围条件、仅在MIN/MAX(分组列)等场景启用;可通过EXPLAIN中“Using index for group-by”判断是否启用松散扫描。

GROUP BY 为什么慢?先看 MySQL 是怎么扫索引的
MySQL 对 GROUP BY 的优化依赖索引扫描方式:松散索引扫描(Loose Index Scan)和紧凑索引扫描(Tight Index Scan)。前者快,后者慢——但不是所有情况都能用上松散扫描。
松散扫描只取每个分组的“第一个值”,跳过组内其余行;紧凑扫描则要遍历所有匹配行再聚合。是否启用松散扫描,取决于索引结构、GROUP BY 字段顺序、是否有聚合函数限制等。
- 松散扫描要求:
GROUP BY列必须是索引最左前缀,且不能有范围条件(如WHERE a = 1 AND b > 10中的b > 10会打断松散扫描) - 如果用了
MIN()/MAX()且参数正好是GROUP BY列本身,MySQL 才可能启用松散扫描;换成MIN(c)就不行 -
ORDER BY和GROUP BY字段一致时,可能复用索引避免排序,但若带LIMIT,反而可能让优化器放弃松散扫描
怎么判断当前用了哪种扫描?看 EXPLAIN 的 Extra 字段
执行 EXPLAIN FORMAT=TREE 或传统 EXPLAIN 后重点看 Extra 列:
- 出现
Using index for group-by→ 成功用了松散索引扫描 - 只有
Using index或Using temporary; Using filesort→ 实际走的是紧凑扫描,甚至临时表 -
Using index condition不代表松散扫描,只是用了 ICP(索引条件下推),和分组效率无关
示例:
EXPLAIN SELECT store_id FROM sales GROUP BY store_id;
如果 sales 表有 (store_id) 单列索引,大概率看到 Using index for group-by;但如果加了 HAVING COUNT(*) > 100,这个提示就消失了——因为 HAVING 需要实际算出每组行数,松散扫描无法满足。
哪些索引能真正帮上 GROUP BY?别只建单列索引
单列索引对 GROUP BY a 有效,但一旦带上 WHERE 或 SELECT 其他字段,就容易失效。真正稳健的做法是构建覆盖型联合索引。
- 基本规则:索引顺序 =
WHERE等值列 +GROUP BY列 +SELECT中需要的非聚合列(用于覆盖) - 例如:
SELECT region, COUNT(*) FROM orders WHERE status = 'shipped' GROUP BY region;→ 建索引(status, region),不是(region) - 如果还查了
amount,且不想回表,索引得是(status, region, amount)——但注意,amount在索引末尾仅对覆盖有用,不影响分组扫描方式 - 范围查询(
>,BETWEEN,LIKE 'abc%')后无法继续用于GROUP BY,所以别把范围列放在联合索引中间
SQL_SMALL_RESULT 和 SQL_BUFFER_RESULT 能不能救急?
这两个查询提示(hint)不改变索引扫描类型,只影响执行策略,适用场景很窄,乱用反而更慢。
-
SQL_SMALL_RESULT告诉优化器“结果集小”,让它倾向用内存临时表而非磁盘临时表——但不会触发松散扫描,也不能绕过WHERE条件对索引的限制 -
SQL_BUFFER_RESULT把中间结果先写入临时表再分组,适合后续要多次读取的场景,但首次执行开销更大,且对大结果集毫无帮助 - 真正该优先做的,是检查
GROUP BY是否必要:能否提前在应用层聚合?能否用物化视图或汇总表替代实时分组?
松散索引扫描不是开关,是索引结构、查询写法、数据分布共同作用的结果。最容易被忽略的一点:即使索引完全匹配,如果统计信息过期(ANALYZE TABLE 没跑),优化器也可能误判而放弃松散扫描。










