加索引后磁盘IO仍高,主因是未覆盖查询字段导致回表随机I/O;需建覆盖索引、避免SELECT*、优化联合索引顺序(过滤字段在前、排序字段在后)并改用游标分页。

为什么加了索引,磁盘IO还是高?
常见现象是:明明给 WHERE 字段加了索引,EXPLAIN 也显示用了 type=ref,但 iostat -x 1 看到 %util 或 await 居高不下,尤其是慢查询日志里反复出现 Using where; Using index condition。
根本原因往往是索引没覆盖查询所需字段,导致回表(random I/O)——MySQL 先查二级索引定位主键,再根据主键去聚簇索引里逐行捞数据。每条记录都是一次随机磁盘寻道。
- 检查是否发生回表:
EXPLAIN FORMAT=JSON查看key和used_columns,对比SELECT列与索引列是否完全重合 - 避免
SELECT *,只查真正需要的字段 - 把高频查询的非索引字段“搬进”索引里,做成覆盖索引:比如原索引是
(user_id),但常查user_id, name, status,就建(user_id, name, status) - 注意索引长度:字符串字段用前缀(如
email(50))可减小索引体积,降低 B+ 树层级,间接减少 IO 次数
ORDER BY + LIMIT 导致全索引扫描?
典型场景是分页接口:SELECT id, title FROM article WHERE status = 1 ORDER BY create_time DESC LIMIT 20。即使 status 和 create_time 都有索引,MySQL 仍可能走 status 索引后排序,或走 create_time 索引但过滤大量 status != 1 的记录。
本质是 MySQL 无法同时高效满足「过滤 + 排序」,尤其当过滤条件选择性差(比如 status = 1 占 90% 数据)时,优化器宁愿扫一遍 create_time 索引再逐行判断 status。
- 建联合索引优先按「过滤字段在前、排序字段在后」:
(status, create_time)—— 这样能直接定位到status = 1的连续块,再按create_time降序取前 20 条 - 如果排序方向不一致(如
ORDER BY create_time ASC, id DESC),联合索引无法生效,考虑改写逻辑或加冗余时间戳字段 - 避免
OFFSET过大:比如LIMIT 10000, 20会先定位并跳过 10000 行,全走索引扫描;改用游标分页(WHERE create_time )
哪些索引反而加重 IO?
不是所有索引都有益。维护索引本身要写磁盘,且某些索引会诱导优化器选错执行路径,引发更重的随机读。
- 低选择性字段单独建索引:比如
gender只有 'M'/'F',索引区分度太低,优化器大概率放弃使用,但每次INSERT/UPDATE仍要更新该索引页 - 重复索引:已有
(a, b),又建(a)—— 后者完全冗余,徒增写放大和缓存压力 - 长文本字段前缀索引过长:比如
content(1000),索引页变大,B+ 树层级增加,一次范围查询可能多扫几层节点 - 监控真实命中率:
SHOW INDEX FROM table_name结合information_schema.INNODB_INDEX_STATS(或 Percona Toolkit 的pt-index-usage)确认索引是否被查询实际用到
innodb_buffer_pool_size 设太小会怎样?
即使索引设计合理,如果缓冲池远小于热数据量,索引页和数据页频繁换入换出,就会把本该是内存访问的请求,变成实实在在的磁盘随机读。
典型表现是 Innodb_buffer_pool_reads(物理读)持续高于 Innodb_buffer_pool_read_requests(逻辑读)的 1%~2%,且 Pages read ahead 较少,说明预读失效、基本靠单页随机读撑着。
- Linux 下建议设为物理内存的 50%~75%,但必须留足空间给 OS 缓存、其他进程及 MySQL 自身开销
- 不要盲目调大:若
buffer_pool_hit_rate已稳定在 99%+,再增大收益极小,反而可能因内存碎片或初始化耗时影响启动 - 观察
Innodb_buffer_pool_wait_free:非零值说明频繁等待空闲页,是 buffer pool 不足的强信号 - 重启生效,线上调整需评估业务低峰期窗口










