explain显示key非空但rows很大,说明mysql虽走了索引,但实际扫描行数极多,反映索引利用效率低,常见于低选择性字段、联合索引顺序错误、范围查询过宽或回表i/o压力大。

EXPLAIN 显示 key 非空,但 rows 大得离谱
这是最常被忽略的信号:执行计划里确实用了索引(key 字段有值),但 rows 列显示扫描了几十万甚至上百万行。MySQL 的“走索引”不等于“只查几行”,它只是表示用了 B+ 树定位起点,后续仍可能扫一大片。
- 典型场景是低选择性字段建了单列索引,比如
gender、status(只有 0/1)、is_deleted—— 即使走了索引,也要回表读取一半甚至更多数据行 - 用
SELECT COUNT(DISTINCT column_name)/COUNT(*) FROM table_name;算下选择性,低于 0.1 就别单独建索引了 - 联合索引顺序错也导致类似问题:比如建了
INDEX idx_a_b (a,b),但查询是WHERE b = ?,MySQL 可能勉强用上索引(type: index),实际是全索引扫描,rows等于索引总行数
Extra 出现 Using where; Using index 和 Using where; Using index condition 的区别
这两个看似都“用了索引”,但性能差一倍不止。关键看是否发生回表。
-
Using where; Using index:覆盖索引,所有字段都在索引里,不用回主键查找 —— 快 -
Using where; Using index condition:ICP(Index Condition Pushdown),MySQL 5.6+ 支持,意思是先用索引过滤一部分,但剩余条件还得回表判断 —— 回表次数多时很慢 - 检查你的
SELECT列和WHERE条件:如果SELECT *或包含非索引列,哪怕WHERE走了索引,照样回表;改成只查索引列,或补全联合索引(如把SELECT id, name+WHERE age = ?→ 建INDEX idx_age_id_name (age, id, name))
明明 type 是 range,却比 ref 还慢
很多人以为 range 比 ref 更高级,其实恰恰相反:range 表示范围扫描,ref 是等值匹配。范围越大,扫描行数越多,尤其当索引列本身区分度低时。
- 例如
WHERE create_time > '2020-01-01',即使有索引,也可能扫几百万行;而WHERE user_id = 12345(主键或唯一索引)永远只查 1 行 -
IN列表过大也会退化为range:MySQL 8.0 对小IN会转成多个ref,但几百个值就变成范围扫描,不如拆成批量JOIN - 避免在时间字段上长期用
>或BETWEEN查历史数据,考虑按月分区,或加AND status = 'active'缩小有效范围
索引没失效,但服务器扛不住回表 I/O
索引本身没问题,问题出在物理层面:每次回表都要随机读一次聚簇索引页,大量回表 = 大量磁盘随机 I/O。
- 现象:
SHOW ENGINE INNODB STATUS里Innodb_buffer_pool_read_requests很高,但Innodb_buffer_pool_reads(物理读)也高,说明缓存没命中 - 根本原因:缓冲池太小,或热点数据集远超
innodb_buffer_pool_size—— 即使 SQL 完美,硬件也拖后腿 - 临时缓解:调大
innodb_buffer_pool_size(建议设为物理内存 50%~70%),但长期要靠覆盖索引减少回表,或分库分表降低单表规模
EXPLAIN 里的 rows 和 Extra 比 key 是否为空重要得多;而服务器配置和数据分布,又常常被开发写完 SQL 后就彻底抛在脑后。











