SELECT COUNT()走索引仍慢,本质是低选择性索引导致大量回表或优化器弃用;应通过Handler_read_验证真实执行路径,改用高选择性字段前置的复合索引或覆盖索引优化。

为什么 SELECT COUNT(*) 走了索引却还是慢?——选择性低的索引本质问题
索引选择性低,不是“建了没用”,而是“用了也白用”。核心在于:MySQL 优化器发现走这个索引后仍需回表大量数据,不如直接全表扫描。典型场景是给 status、is_deleted、gender 这类只有几个取值的字段建了单列索引。即使 EXPLAIN 显示 type=ref 或 range,实际执行时也可能因 rows 预估过大而放弃使用,或虽用了但 I/O 开销不降反升。
判断索引是否真被用上:别只看 EXPLAIN,要查 Handler_read_*
优化器决策依赖统计信息,而 EXPLAIN 只是预估。真正验证是否走索引,得看运行时的存储引擎计数器:
SHOW STATUS LIKE 'Handler_read%';
重点观察:Handler_read_key(索引查找次数)和 Handler_read_next(索引顺序扫描次数)是否明显上升;同时对比 Handler_read_rnd_next(回表随机读次数)是否暴增——如果后者远高于前者,说明索引虽被选中,但导致大量回表,性能反而恶化。
- 执行查询前先
FLUSH STATUS - 再跑一次目标 SQL
- 立刻查
Handler_read_*,比看EXPLAIN的key字段更可靠
提升选择性的实操路径:不是删索引,而是换建法
对低选择性字段,强行加单列索引基本无效。有效做法是把它作为复合索引的「后缀」,而非前导列:
- 错误写法:
INDEX idx_status (status)→ 几乎无用 - 正确思路:找出常和
status一起过滤的高选择性字段,比如created_at、user_id,构建INDEX idx_user_status (user_id, status)或INDEX idx_time_status (created_at, status) - 注意顺序:
WHERE user_id = ? AND status = ?必须把高选择性列放前面,否则索引无法命中前缀 - 若查询含
ORDER BY status,且status是唯一排序字段,那复合索引也救不了——此时应考虑归档冷数据或改用物化视图/汇总表
替代方案:覆盖索引 + 条件过滤,绕过低选择性瓶颈
当查询只涉及少量字段,且其中包含低选择性列时,优先走覆盖索引,彻底避免回表:
SELECT id, status, updated_at FROM orders WHERE status = 'pending' ORDER BY updated_at DESC LIMIT 20;
对应索引应为:INDEX idx_cover (status, updated_at, id)。这样 MySQL 可在索引 B+ 树里完成全部查找、排序、取值,Handler_read_rnd_next = 0。
但要注意:覆盖索引会增大索引体积,写入开销上升。如果 status 更新频繁,且该索引又很宽,可能引发页分裂和缓冲池压力。上线前务必在压测环境对比 innodb_buffer_pool_reads 和 innodb_pages_written 的变化。
低选择性本身不可怕,可怕的是把它当成独立优化点去硬扛。它往往是业务模型或数据分布的信号——比如 status IN ('pending', 'processing') 返回 95% 的行,那问题不在索引,而在是否真需要实时查这 95%。










