结论:GROUP BY 触发全表扫描主因是松散索引扫描失效,根源在于索引设计不当或SQL写法破坏其启用条件;需通过EXPLAIN检查type是否为ALL、Extra是否含Using temporary/filesort来确认。

直接说结论:GROUP BY 触发全表扫描,90% 是因为没走松散索引扫描(Loose Index Scan),本质是索引没建对、或 SQL 写法破坏了优化器启用该算法的条件。
怎么判断是不是松散索引扫描失效了
先跑 EXPLAIN,重点看两处:
-
type字段不是index或range,而是ALL→ 全表扫描坐实 -
Extra出现Using temporary或Using filesort→ 没走松散扫描,被迫建临时表+排序
注意:Using temporary 不一定等于“用了磁盘临时表”,但一定意味着 MySQL 没法靠索引顺序直接分组,必须缓存中间结果。
为什么加了索引 GROUP BY 还是全表扫
常见原因不是“没索引”,而是索引和查询不匹配。松散索引扫描极其娇气,以下任一条件不满足就会退化:
-
GROUP BY字段不是索引最左前缀,比如索引是(status, user_id, created_at),但写GROUP BY user_id -
WHERE里有范围条件且不在最左,比如索引(user_id, created_at),但写WHERE created_at > '2024-01-01' GROUP BY user_id→ 只能走紧凑扫描(Tight Index Scan),扫所有符合条件的行 - 用了不被支持的聚合函数,比如
SUM(amount)或AVG();松散扫描只认COUNT(*)、MIN()、MAX() - 字段类型隐式转换,比如
user_id是VARCHAR,但WHERE user_id = 123传了整数 → 索引失效,连带松散扫描失效
建什么索引才能让 GROUP BY 走松散扫描
核心原则:索引顺序 = WHERE 等值条件字段 + GROUP BY 字段 + SELECT 中非聚合字段或 MIN/MAX 目标字段
- 简单场景:
SELECT user_id, MAX(created_at) FROM orders GROUP BY user_id→ 建索引(user_id, created_at) - 带等值过滤:
SELECT category, COUNT(*) FROM products WHERE status = 'on_sale' GROUP BY category→ 索引必须是(status, category),不能反过来 - 避免冗余:不要在索引末尾加无关字段,比如
(user_id, created_at, amount)用于GROUP BY user_id,amount白占空间、拖慢写入
特别提醒:ORDER BY 如果和 GROUP BY 字段一致(如都按 user_id),不会额外开销;但如果写成 ORDER BY created_at,即使 created_at 在索引里,也可能让优化器放弃松散扫描。
实在没法走松散扫描时的兜底方案
当业务逻辑强制要用 SUM()、或 WHERE 必须带范围、或字段无法改类型时,别硬扛全表扫:
- 用子查询先定位每组“代表性主键”,再关联回原表,例如:
SELECT o.user_id, o.amount FROM orders o JOIN (SELECT user_id, MAX(id) AS id FROM orders GROUP BY user_id) tmp ON o.id = tmp.id - 对高频分组结果做物化缓存(如 Redis 存
user_id → total_amount),更新时异步刷新 - 调大
tmp_table_size和max_heap_table_size,至少让临时表留在内存,避免写磁盘
真正容易被忽略的是:松散索引扫描不保证永远生效——它依赖优化器成本估算,而估算可能因统计信息过期、数据倾斜、版本升级行为变更而失效。上线后务必用真实数据量 + EXPLAIN FORMAT=TRADITIONAL 验证,不能只看本地小表测试结果。


















