EXPLAIN显示用了索引但查询仍卡在磁盘,是因为索引页或数据页未缓存在InnoDB Buffer Pool中,导致必须从磁盘读取;机械盘随机IO延迟5–15ms、SSD也需0.1–0.3ms,大量回表或冷数据加载会显著拖慢响应。

为什么 EXPLAIN 显示用了索引,但查询还是卡在磁盘上?
因为「用索引」只说明优化器选了某个索引去定位数据,不代表实际执行时能从内存里快速拿到。如果索引页或数据页不在 InnoDB Buffer Pool 里,就得从磁盘读——而机械盘随机 IO 延迟通常在 5–15ms,SSD 也要 0.1–0.3ms,10 万行扫描就可能拖到秒级。
常见表现:SELECT 执行时间波动大、SHOW ENGINE INNODB STATUS 里 Buffer pool hit rate 长期低于 95%、iostat -x 1 显示 %util 持续 >90% 或 await >10ms。
- Buffer Pool 太小(比如默认 128MB),远小于热数据集大小
- 查询反复触发冷数据加载,比如按时间范围查历史归档表,且该范围的数据页从未被缓存过
- 大量写入导致 Buffer Pool 频繁刷脏页,挤占读缓存空间
- 使用了
SELECT *或大字段(如TEXT),即使走索引也要回表读聚簇索引,放大 IO 次数
怎么快速确认是不是磁盘 IO 瓶颈?
别猜,用系统工具直接看实时指标。重点不是「有没有 IO」,而是「IO 是不是等出来的」。
在数据库服务器上运行:
$ iostat -x 1 Device: r/s w/s rkB/s wkB/s await svctm %util nvme0n1 240 12 32400 210 4.2 0.8 20%
关键看三列:await(平均每次 IO 等待毫秒数)、svctm(真正服务耗时)、%util(设备忙时占比)。如果 await ≫ svctm(比如 20ms vs 0.3ms),说明请求在队列里排队;%util 接近 100% 且 await 持续升高,基本就是磁盘扛不住了。
- 搭配
iotop看哪个进程在刷盘(常是mysqld的后台线程) -
vmstat 1中观察bi(块输入 KB/s)是否持续高于磁盘吞吐能力 - MySQL 内部可查
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests'和'Innodb_buffer_pool_reads',后者是真从磁盘读的次数,前者是总逻辑读次数;比值 > 5% 就说明命中率差
InnoDB Buffer Pool 设置多大才够?
不是越大越好,也不是随便设个 70% 物理内存就完事。核心是让「活跃工作集」尽量驻留内存。
先估算热数据量:
- 查
SELECT CEILING(TOTAL_LENGTH/1024/1024) AS size_mb FROM (SELECT SUM(data_length + index_length) AS TOTAL_LENGTH FROM information_schema.tables WHERE table_schema = 'your_db') t; - 再结合业务访问模式:比如每天只查最近 7 天订单,那热数据≈7天的表+索引大小,不是全库
然后设 innodb_buffer_pool_size 至少为该值的 1.2–1.5 倍,并确保 OS 还有足够内存给文件系统缓存和 MySQL 其他线程(如排序、临时表)。
注意:innodb_buffer_pool_size 修改后必须重启 MySQL 生效;在线扩容(MySQL 5.7+)需分步做,且不能低于当前已用大小,否则会报错。
索引用了但依然慢,还有哪些 IO 相关的坑?
索引本身也可能成为 IO 瓶颈源,尤其当它太大或结构不合理时。
- 覆盖索引没做全:比如
WHERE a=1 ORDER BY b,但索引只建了(a),没包含b,就会导致索引扫描后还要回表排序,产生大量随机读 - 索引列顺序反了:复合索引
(created_at, status)对WHERE status=1 AND created_at > '2026-04-01'效果很差,因为无法跳过status的低选择性过滤 - BLOB/TEXT 字段建了前缀索引但前缀太短,导致大量重复值,索引树深度增加,查找路径变长
- 使用
LIKE '%xxx'触发索引失效后,优化器可能误判为“用索引比全表扫还慢”,干脆放弃索引,退化成磁盘全扫
最隐蔽的一点:即使所有数据都在 Buffer Pool 里,如果并发高、锁争用严重(比如 UPDATE 频繁更新同一行),也会让查询线程长时间等待行锁,表现为“看着像 IO 慢”,其实是锁在排队。


















