索引失效是优化器因代价高主动弃用索引,非索引被删;GROUP BY需最左前缀索引且顺序一致;HAVING无法下推故应前置WHERE过滤;COUNT(*)在InnoDB中未必走主键索引;ORDER BY+LIMIT需覆盖索引避免临时表。

聚合查询中索引失效不等于“索引被删了”,而是优化器发现走索引的代价比直接扫表还高,于是主动选了 type: ALL —— 这在 GROUP BY、ORDER BY、COUNT(*) 等场景特别常见。
GROUP BY 字段没索引或顺序错
MySQL 对 GROUP BY 的执行依赖排序或松散索引扫描(Loose Index Scan),但前提是:GROUP BY 列必须是索引的最左前缀,且不能混用 ASC/DESC(MySQL 8.0 前尤其敏感)。
- ❌ 失效示例:
SELECT city, COUNT(*) FROM users GROUP BY city,但索引是(status, city)→ 跳过status,无法定位起点 - ✅ 有效写法:建联合索引
(city, status),或单独索引city - ⚠️ 注意:
key_len在 EXPLAIN 中为 0 或远小于预期值,说明索引没被用于分组;若Extra出现Using temporary; Using filesort,基本已退化为内存/磁盘排序+全表扫
HAVING 过滤写在 GROUP BY 后面
HAVING 是对分组结果的过滤,不是对原始行的过滤。如果条件里用了非索引字段、函数或表达式,优化器无法下推到索引扫描阶段,只能先全量分组再过滤。
- ❌ 失效示例:
SELECT user_id, COUNT(*) FROM orders GROUP BY user_id HAVING YEAR(MAX(created_at)) = 2025→YEAR()作用于聚合结果,索引完全无用 - ✅ 改法:把时间过滤提前到
WHERE,例如WHERE created_at >= '2025-01-01',让索引能先筛掉大部分数据 - ? 补充:
HAVING中用索引列做等值判断(如HAVING user_id > 1000)也无效,因为分组后user_id已不是原始索引顺序
COUNT(*) 和主键索引的误解
很多人以为 COUNT(*) 必走主键索引,但实际是否全表扫描取决于存储引擎和表结构:
- ❌ MyISAM 表确实能快速返回行数(元数据缓存),但 InnoDB 不行——它必须确认每行是否可见(MVCC),所以即使有主键索引,
COUNT(*)仍可能触发索引遍历(type: index)甚至全表扫描(type: ALL) - ✅ 若只查行数且可接受近似值,用
SHOW TABLE STATUS LIKE 't'查Rows字段(MyISAM 精确,InnoDB 是估算) - ⚠️ 注意:加了
WHERE的COUNT(col)如果col允许 NULL,且没索引,就会全表扫;而COUNT(*)或COUNT(pk)可能走主键索引,但前提是优化器认为它比扫二级索引更优
ORDER BY + LIMIT 混合聚合时索引被“截断”
当聚合后还要排序+分页(比如热门标签 TOP 10),如果 ORDER BY 字段不在 GROUP BY 索引中,或者顺序冲突,优化器会放弃索引分组,改用临时表。
- ❌ 失效组合:
SELECT tag, COUNT(*) c FROM posts GROUP BY tag ORDER BY c DESC LIMIT 10,但索引只有(tag)→ 分组快,但排序要额外filesort,优化器可能干脆全扫+内存排序 - ✅ 有效索引:
(tag, id)不够,得建覆盖索引(tag, created_at)并配合 WHERE 限时间范围;更稳的是(tag) INCLUDE (id)(PostgreSQL)或 MySQL 8.0+ 的函数索引模拟 - ? 关键信号:EXPLAIN 中
Extra同时出现Using temporary和Using filesort,说明聚合和排序都没走索引加速
真正容易被忽略的是:聚合语义本身会削弱索引能力——索引优化针对的是“单行定位”,而 GROUP BY / ORDER BY / HAVING 都在操作逻辑分组或结果集,一旦中间步骤无法下推,前面建再多索引也没用。验证永远从 EXPLAIN 的 type 和 Extra 开始,而不是看有没有建索引。

















