MySQL 索引为什么没按预期生效:SQL 明明走了索引,怎么还是慢
建了索引、执行计划里也能看到 key,但 SQL 还是慢,问题往往不在“索引有没有生效”,而在过滤、排序、扫描范围或回表这几步里,哪一步没有按预期接上。
线上排查慢 SQL 时,经常会遇到一种很别扭的情况:
- 这个字段明明建了索引
EXPLAIN里也不是完全没走索引- 但 SQL 还是慢,RT 还是下不来
很多人这时候会直接下结论:索引失效了。
可真实现场里,问题往往没这么简单。
更多时候,不是索引完全没用上,而是你原本期待数据库这样执行:先靠索引把数据范围迅速缩小,再顺着索引把排序接上,最后低成本拿到结果;结果真正执行时,某一步没有按预期接上,整条 SQL 就开始变慢了。
所以这篇不再讲“8 个常见原因”。如果你现在面对的是一条“明明建了索引,怎么还是慢”的 SQL,更有用的排查方式是直接看执行计划:数据库到底在哪一步,没按你以为的路径走。
先别急着看“走没走索引”,先看你原本以为它会怎么跑
比如订单列表有这样一条查询:
select id, order_no, amount, create_time
from orders
where user_id = 1001
and status = 'PAID'
order by create_time desc
limit 20;
表上有联合索引:
idx_user_status_ctime(user_id, status, create_time)
很多人看到这里,会默认觉得这条 SQL 应该很顺。
因为在脑子里,它大概会这样执行:
- 先用
user_id找到这个用户的订单 - 再用
status把已支付订单筛出来 - 顺着索引里的
create_time倒序取前 20 条 - 中间不用额外排序,也不用多扫很多无关数据
真正排查时,关键不是看“字段都在索引里”,而是看执行计划有没有把这 4 步真的连起来。
只要其中有一步断了,这条 SQL 就会出现一种常见错觉:索引看起来在,但速度没有按预期好起来。
第一种情况:执行计划里虽然有 key,但索引根本不是你以为的那个入口
还是这个联合索引:
idx_user_status_ctime(user_id, status, create_time)
如果 SQL 变成这样:
select id, order_no
from orders
where status = 'PAID'
order by create_time desc
limit 20;
很多人会困惑:status 和 create_time 不是也都在索引里吗?
问题在于,你期待的那条索引路径,入口其实是 user_id。现在 SQL 一上来并没有先限定 user_id,数据库就很难顺着这条联合索引,把过滤和排序一路接下去。
这时执行计划里可能仍然会出现某个索引,但那不代表它走的是你脑子里那条“先过滤、再顺序取数”的理想路径。
这里真正要看的不是“有没有 key”,而是:
- 选中的
key是不是你以为能承接整条查询的那条索引 type、rows有没有说明它其实扫了很大一片范围Extra里是不是开始出现额外排序或额外过滤
这一类慢,不是因为“索引字段不够”,而是查询起点和索引起点不是一回事。
第二种情况:过滤条件写上去了,但你把索引可用的比较方式改掉了
这类问题在代码里特别常见,因为 SQL 看起来完全合法。
比如:
select id, title
from article
where date(create_time) = '2026-03-20';
或者:
where cast(user_id as char) = '1001'
或者:
where amount * 100 > 5000
这几种写法有个共同点:你看到的是“条件里用了索引字段”,但数据库真正面对的已经不是原始列值,而是一个表达式结果。
一旦变成先算再比较,索引原本那种可直接定位、可顺序扫描的优势,往往就接不上了。
所以如果执行计划表现得很差,这时候不要停在“为什么字段有索引却没用上”,而要把问题说得更具体一点:
这条 SQL 不是没写索引字段,而是没有按索引原本能利用的方式去比较。
比如按天查时间,更稳的写法通常是:
select id, title
from article
where create_time >= '2026-03-20 00:00:00'
and create_time < '2026-03-21 00:00:00';
这样改的重点,不是为了“语法规范”,而是为了让数据库还能沿着索引顺序去找,而不是把整列先算一遍再判断。
第三种情况:过滤那一步还行,但排序已经没接上
很多“明明有索引还是慢”的现场,真正拖垮性能的不是过滤,而是排序。
看这条 SQL:
select id, order_no, amount, create_time
from orders
where status = 'PAID'
order by create_time desc
limit 20;
假设表上只有这两个单列索引:
idx_status(status)
idx_create_time(create_time)
这时候很多人的直觉是:过滤列有索引,排序列也有索引,怎么还会慢?
因为数据库执行的不是拼图游戏,不是两个索引都存在就能自动把效果叠起来。你真正希望它做的是:先筛出 status = 'PAID',再顺着同一条有序路径直接拿到最新的 20 条。
如果过滤和排序不是同一条索引在承接,执行计划里就很容易出现这种情况:
- 先用
idx_status找出大量已支付订单 - 再把结果单独排序
Extra里出现Using filesort
这时 SQL 慢,不是因为过滤完全没走索引,而是过滤之后,排序那一步已经掉到索引外面去了。
所以碰到 where + order by + limit 这类查询,别只盯着 key 看。还要继续问:
- 过滤和排序是不是由同一条索引顺着完成的
- 还是说过滤完以后,数据库还得单独再排一次
如果是后者,索引就只是帮了一半。
第四种情况:执行计划显示走了索引,但扫描量还是大得离谱
有时执行计划最容易误导人的地方,就是 key 不是空的。
大家一看到这里,第一反应往往是:那至少索引生效了。
但如果你同时还看到这些信号,就要小心:
rows依然很大- 实际只需要几十条结果
Extra里还有Using where、Using filesort或Using temporary
比如:
select id, order_no, amount, create_time
from orders
where status = 'PAID'
order by create_time desc
limit 20;
执行计划可能像这样:
key: idx_status
rows: 180000
Extra: Using where; Using filesort
这说明数据库确实用了 status 这个索引,但它仍然要扫出 18 万条候选记录,再从里面继续过滤、继续排序。
这种场景下,说“索引生效了”不算错,但对排查没什么帮助。因为真正的问题是:这条索引虽然参与了执行,但并没有把成本压到你想要的程度。
所以判断“索引为什么没按预期生效”时,我一般至少会连着看这几个点:
key选了谁rows估计会扫多少Extra里有没有额外排序、临时表、额外过滤
只看 key,特别容易把“索引参与执行”误看成“索引已经解决问题”。
第五种情况:前面都对了,但回表把成本又抬起来了
还有一种情况,经常出现在列表页查询里。
比如:
select *
from orders
where user_id = 1001
order by create_time desc
limit 20;
假设 (user_id, create_time) 这条索引确实能承接过滤和排序,那这条 SQL 是不是就一定够快?也不一定。
因为你查的是 select *。数据库就算先沿着索引找到了目标记录,后面往往还要再回表,把整行数据取出来。
如果候选记录多、字段很多,或者行本身很宽,这一步的成本就会重新冒出来。
于是你看到的现象就会很容易让人困惑:
- 过滤字段有索引
- 排序字段也接上了
- 执行计划看起来不算特别差
- 但响应时间还是不够理想
这时要怀疑的,就不只是“有没有走索引”,而是:索引只负责把记录位置找出来,真正取数据那一步是不是还很重。
也就是说,索引路径前半段没有明显问题,不代表整条查询已经便宜了。
第六种情况:你以为还在看索引问题,其实已经是查询模型本身太重
再往后排查,有些 SQL 会发现问题已经不只是索引接没接上,而是查询本身就很贵。
比如深分页:
select id, order_no, create_time
from orders
where user_id = 1001
order by create_time desc
limit 100000, 20;
再比如前导模糊匹配:
where name like '%yuan%'
这些场景里,你当然还是可以继续看执行计划,但如果脑子里一直只想着“是不是索引失效”,通常会越看越别扭。
因为真正的问题已经变成:
- 这类查询是不是本来就不适合靠普通 B+ 树索引低成本完成
- 就算某个索引参与了,整体代价是不是还是很高
- 该不该改分页方式、检索方式,或者干脆调整交互设计
也就是说,有些 SQL 的慢,不是因为索引突然没用了,而是因为你期待它解决的,已经不是索引最擅长解决的那类问题。
真正有用的问题,不是“索引失效了吗”,而是“哪一步和预期不一样”
如果你下次又遇到“这个字段明明建了索引,为什么还是慢”,我更建议直接拿 EXPLAIN 对照这几个问题看:
- 过滤是不是从你以为的索引入口开始的
- 条件比较方式有没有把索引顺序打断
- 排序有没有沿着同一条索引接下去
- 扫描量是不是仍然大得不合理
- 回表是不是把前面省下来的成本又抬回去了
- 这条 SQL 是不是已经属于查询模型本身太重
这样看,你就不会只停在一句空泛的“索引失效”。
你会更清楚地知道:这条 SQL 到底是入口不对、排序断了、扫描太大,还是回表和查询模型本身在拖后腿。
很多慢 SQL 真正需要的,不是一张“原因清单”,而是把执行计划里那一步和你预期不一致的地方找出来。
最后
所以我现在再遇到“明明建了索引,SQL 怎么还是慢”,基本不会先问“是不是索引失效了”。
我更关心的是:你原本期待数据库顺着索引完成的那条路径,到底断在了哪一步。
只要这件事说清楚,后面的优化方向通常也就清楚了:是要改联合索引顺序,改写条件,补齐过滤和排序的一致性,控制扫描范围,还是回头承认这条查询本身就不适合这样跑。
把问题定位到这一步,比背多少个“常见原因”都更有用。