为什么删掉索引后,查询瞬间变快了?探讨MySQL的随机I/O与顺序I/O博弈

大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!

前几天,有个开发同学火急火燎地来找我:"小耶,见鬼了!我给 status 字段加了个索引,查询不但没变快,反而从 0.5 秒变成了 5 秒!把索引一删,又秒回了 0.5 秒。"

"加索引反而慢,删索引反而快?"

这听起来完全违背常理。在很多人的直觉里,B+树索引不就是用来加速查询的吗?为什么加了反而成了拖油瓶?

今天,我们就来扒一扒 MySQL 内核里一个极具欺骗性的机制:回表代价。顺便看看优化器到底是怎么在背后"算计"我们的。

先搞清楚一个背景:

InnoDB 有两大核心索引:聚簇索引(主键索引) 和 二级索引(辅助索引)。

  • 聚簇索引 :叶子节点存的是整行数据。

  • 二级索引 :叶子节点存的是索引列的值 + 主键ID。

当你写 SELECT * FROM orders WHERE status = 'DONE' 时,如果走了二级索引,MySQL 需要先在 status 索引树上找到所有 DONE 的主键ID,然后再拿着这些ID去主键索引树上查完整的行数据。

这个过程,就叫回表。

一、为什么优化器会"放弃"你的索引?

假设我们有一张 1000 万行的订单表,status 的状态分布如下:

  • DONE:900 万行(占 90%)

  • PENDING:90 万行

  • CANCELLED:10 万行

现在你要查 status = 'DONE'。

方案A:走二级索引 + 回表

  1. 在 status 索引树上,扫描出 900 万个主键ID。

  2. 拿到这 900 万个ID后,回到主键(聚簇)索引树里,进行 900 万次随机I/O查找。

方案B:全表扫描

直接顺着主键的聚簇索引,从头到尾扫一遍,把每一行读出来,判断 status 是不是 DONE。这是 1000 万次顺序I/O。

在机械硬盘或普通的SSD上,随机I/O 的速度是远远低于顺序I/O 的。

优化器(CBO)估算了一下代价:900 万次回表产生的随机 I/O 成本,甚至超过了 1000 万次顺序全表扫描。于是,优化器果断决定:"这索引不用了,直接全表扫!"

这就是为什么你明明建了索引,EXPLAIN 里的 type 却显示 ALL,key 显示 NULL。

如何验证?

用 EXPLAIN 查看执行计划,你会发现 possible_keys 里有这个索引,但 key 是 NULL。这就实锤了优化器主动放弃了它。

二、如果强制用索引,为什么会慢 10 倍?

有时候,如果你用 FORCE INDEX 强制 MySQL 走索引,或者 MySQL 8.0 的优化器由于统计信息过期硬选了索引,性能就会惨烈翻车。

为什么?因为你在业务代码里写了 SELECT *,导致 MySQL 必须进行 900 万次回表。这就有了开头那一幕:0.5 秒的全表扫描,变成了 5 秒的随机乱跳。

三、终极解法:消灭回表(覆盖索引)

既然瓶颈在"回表",解法就很明确了:不要回表。

如果我们在索引树上,就能拿到业务需要的所有列,就完全不需要去聚簇索引里找完整行了。这叫覆盖索引(Covering Index)。

比如,业务其实只需要查订单号和金额:

bash 复制代码
SELECT id, amount FROM orders WHERE status = 'DONE';

我们可以建一个联合索引:

bash 复制代码
ALTER TABLE orders ADD INDEX idx_status_amount (status, amount);

这时候,索引的叶子节点上,存的是 status、amount 和 id。

MySQL 在 idx_status_amount 里扫完 900 万个数据,直接返回即可。整个过程只有顺序读取索引页,没有任何随机 I/O。

看 EXPLAIN 的结果:Extra 列里会出现一个极度舒适的字眼:Using index 。

实测数据对比:

  • 全表扫描:0.52 秒

  • 强制走普通索引(大量回表):4.85 秒

  • 走覆盖索引(无回表):0.02 秒

四、避坑指南:怎么写对覆盖索引?

  1. 尽量不要写 SELECT *。这是导致回表的最直接元凶。只查你需要的列。

  2. 联合索引的设计。通常把 WHERE 条件里的列放在前面,把 SELECT 里的列放在后面。

  3. 不要盲目建索引。如果一个字段的基数(不同值的数量)极低,比如性别(男/女)、布尔状态(0/1),单独建索引毫无意义。如果业务经常查低频值(比如 PENDING),可以只建这个值的部分索引(MySQL 8.0 暂不支持,但可以走其它优化),或者把高频字段放在联合索引的最左侧。

五、小结

优化器不是傻子,它比人更懂 I/O 代价。加索引反而慢,通常是因为大比例数据查询带来的"回表随机 I/O"压垮了性能。看执行计划时,别只盯着有没有走索引,更要看 Extra 里有没有 Using index。能用覆盖索引解决的,绝不让它回表!

小耶在手,SQL 不愁

还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽......我们下次见~

相关推荐
kingx34 小时前
BugKu 渗透测试 4 实战:文件上传打点,5 Flag 全通
数据库·渗透测试·web·内网渗透
我命由我123454 小时前
Python 实用操作 - 文件操作(打开与关闭文件、读取文件、文件写入)
java·开发语言·前端·javascript·数据库·python·java-ee
加宝的起点雷达5 小时前
40 行原子写入:为什么临时文件必须写在同一个目录
人工智能·代码规范
SelectDB5 小时前
Apache Doris 与 SelectDB 是什么关系?从开源技术到面向 Agent 的极速分析数据库
大数据·数据库·数据分析
VimorphTech5 小时前
文本已经改了,旧的音频生成结果还能回写吗?
java·数据库·mybatis
SelectDB5 小时前
存算分离冷查询提速 7.5 倍!Apache Doris 4.2 Peer Read 实现跨计算组缓存复用
大数据·数据库·数据分析
盛世宏博智慧档案5 小时前
一站式智慧档案解决方案,如何实现防火防盗漏水多维度安全监测实践
服务器·网络·数据库
GreatSQL5 小时前
GreatSQL 8.0回流MySQL 5.7主从复制中断分析排查过程
mysql
数据库小学妹5 小时前
MySQL磁盘IO瓶颈:iostat与刷脏fsync
数据库·mysql·性能优化