做了几年后端开发,大家早晚都会撞上一次这种场景:线上一个订单查询接口,原本几十毫秒返回,某天突然变成两三秒,赶上网购大促那几天更是时不时慢查询告警。运维把慢查询日志甩过来,SQL一看,条件不过是个user_id,数据量也就几千万,怎么就走不动了呢?
这类问题的答案,十有八九落在MySQL索引优化上。索引优化是数据库调优里性价比最高的一块,它不需要你重构表结构,也不用购买更高配置的机器,把索引建对、把SQL改顺,同样的语句往往能从几秒降到几十毫秒。这篇文章不打算堆砌高深理论,我会从索引最底层的结构讲起,结合真实的执行计划排查过程,把“慢SQL如何排查—索引为什么生效—哪些场景会失效—实战怎么优化”这条链路完整串起来。适合刚接触MySQL索引优化的新手、写SQL但没深究过执行计划的开发,以及正在准备面试、想系统梳理索引知识的同学。
1. 一个慢SQL引出的问题:为什么行数不多却要扫全表
1.1 当时那条SQL和数据表
先还原一下我遇到的真实场景。订单表结构大概是这样(已简化):
sql复制CREATE TABLE `order_info` (
`id` bigint NOT NULL AUTO_INCREMENT,
`order_no` varchar(32) NOT NULL,
`user_id` bigint NOT NULL,
`amount` decimal(10,2) NOT NULL,
`status` tinyint NOT NULL DEFAULT '0',
`create_time` datetime NOT NULL,
`pay_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
表中大概有两千万行数据,user_id有50万左右个不同用户。慢查询是这条:
sql复制SELECT * FROM order_info
WHERE user_id = 10001
ORDER BY create_time DESC
LIMIT 10;
最让人意外的地方在于:user_id字段上明明建了索引idx_user_id,可执行计划显示扫描的行数高达八十多万行,还出现了Using filesort。换句话说,MySQL没有通过user_id索引直接定位到那几行,而是扫了大量数据再去排序。
1.2 数据量没到“大”的程度,问题出在哪
两千万行在MySQL里并不算特别夸张,InnoDB完全可以支撑。问题不在于表多大,而在于这条查询的扫描方式让量变发生了质变。
为了说清这条SQL为什么会走成全表扫描,需要先理解MySQL执行一次查询的基本流程:解析SQL、生成执行计划、按照执行计划去InnoDB存储引擎读取数据。执行计划里有一个关键环节叫row estimate,也就是优化器会估算每种方案要扫描多少行,然后选择它认为代价最小的方案。
当user_id = 10001这个条件过滤性很差的时候——比如这个用户下了几十万单——MySQL优化器一算,走idx_user_id要回表几十万次,还不如直接全表扫描来得省事。这个判断本身没有错,错就错在SQL写法没有给优化器提供更好的选择。
1.3 索引优化解决的其实是一个成本和选择问题
索引优化的本质,是帮助优化器用更少的IO、更小的代价拿到命中的数据。在InnoDB中,数据是按B+树组织的,每个节点对应一个16KB的页。我们要做的就是让查询能走上一棵高度足够低、节点足够紧凑的B+树,而不是在一个庞大的页链表上从头扫到尾。
这条SQL最终应该怎么优化,我先留个悬念,到实战案例章节再展开。这里先铺垫一个观念:**排查慢查询不能只看“有没有索引”,还得看“索引有没有被用上”以及“用上之后代价是不是足够小”。**这个观念会贯穿整篇文章,下面几章逐个展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 索引为什么能快:从B+树到磁盘IO的底层逻辑
不把索引的底层机制搞清楚,后面所有的优化手段都只是背结论。这一章我尽量用通俗的话把B+树讲透。
2.1 先从“页”说起:16KB的读写单位
InnoDB读取磁盘的最小单位不是一行记录,而是一个页(page),默认大小16KB。也就是说,哪怕你只需要一行数据,MySQL也会把这个16KB的页整个从磁盘读进内存。
这带来了一个重要推论:一个页里能装多少数据行,直接决定了需要读多少个页。假设一行数据平均1KB,一个页能装16行;如果要扫描1万行,大约要读625个页。而如果是走索引,通过主键定位到某一行,可能只需要从根节点往下读2~3个索引页,外加1个数据页,也就是总共3~4次磁盘IO。
索引之所以快,就是把“读多少页”这个问题从“接近全表页数”压缩到了“树的高度+1”。
2.2 为什么是B+树,而不是哈希表、红黑树、B树
很多人背书能说出“MySQL用B+树”,但被问到“为什么”就卡壳。这里给一个比较完整的对比视角。
哈希索引:等值查询的时间复杂度是O(1),确实能做到极快。但哈希结构天然无序,无法支持范围查询、不能排序,也不支持前缀匹配。大部分业务SQL都不是纯粹的等值条件,ORDER BY、BETWEEN、LIKE 'abc%'太常见了。所以InnoDB默认的索引结构不能选哈希。
红黑树:它是平衡二叉搜索树,高度约log2(N)。两千万行数据,树高大约是25层。这个高度看起来不高,问题在于每一层的节点都可能不在同一个磁盘页里,查找一个叶子节点的数据,可
