很多人一看到 SQL 慢,第一反应就是:
“给这个字段加个索引。”
结果索引加了,查询还是慢;
有时候甚至 EXPLAIN 一看,索引都用上了,但执行时间依旧不好看。
这时候问题往往不在“有没有索引”,而在于另一个更关键的点:
你以为你在优化索引,实际上你是在和查询方式、数据分布、执行路径打架。
我见过很多项目都是这样:
- 表里明明有索引,还是全表扫
- 走了索引,但扫描行数依然巨大
- SQL 看起来很简单,实际慢点根本不在 where,而在排序、分页、回表、锁等待
所以,数据库慢,很多时候不是“没加索引”,而是对索引的理解还停留在“加了就会快”这个阶段。
今天我想聊 3 个最容易被忽略的细节。
这 3 个点,基本上就是很多慢 SQL 的真正根源。
一、不是加了索引就行,而是你的 SQL 真正“用对了索引”没有
很多人说“这个字段我明明加了索引,为什么还是慢?”
先别急着怪数据库,先看一句最现实的话:
索引不是你建了就一定会用,只有你的 SQL 写法适合它,优化器才可能选它。
最常见的几个坑,几乎每个项目都见过。
1. 对索引列做了函数、计算、表达式处理
比如:
SELECT * FROM ordersWHERE DATE(create_time) = '2026-03-06';看起来挺自然,但这类写法很容易让索引失效。
因为你不是在直接比较 create_time,而是先对列做了 DATE() 处理。
数据库想走索引,最喜欢的是“原值可直接定位”的条件。

更稳的写法应该是:
SELECT *FROM ordersWHERE create_time >= '2026-03-06 00:00:00' AND create_time < '2026-03-07 00:00:00';这类范围查询,通常比在列上套函数更容易利用索引。
2. 隐式类型转换,把索引悄悄搞没了
例如字段是字符串:
phone VARCHAR(20)但你这样查:
SELECT *FROM customerWHERE phone = 13800138000;表面没问题,实际上数据库可能会做类型转换。
一旦发生隐式转换,索引使用效果就可能变差,甚至直接放弃。
尤其在业务系统里,这种问题太常见了:
- 数据库字段是 varchar
- Java 里传的是 Long
- MyBatis / JPA 帮你拼出来看似正常
- 最后执行计划却不如预期
这类问题很隐蔽,因为 SQL 能跑,结果也对,但性能就是不对。
3. 联合索引建了,但 where 条件顺序和使用方式不对
比如你建了一个联合索引:
INDEX idx_user_status_time (user_id, status, create_time)你以为下面这些都会快:
WHERE user_id = ?WHERE status = ?WHERE create_time > ?WHERE status = ? AND create_time > ?其实不是。
联合索引通常要看最左前缀。
它更像是先按 user_id 排,再按 status 排,再按 create_time 排。
所以最有效的往往是这种:
WHERE user_id = ?AND status = ?AND create_time > ?而如果你跳过前面的列,直接查后面的列,索引效果通常会大打折扣。
这一点很多人容易误判
不是说索引“没建对”,
而是你建的是一条路,SQL 却没按这条路走。
二、就算走了索引,也不代表一定快
这是第二个特别容易误解的点。
很多开发一看 EXPLAIN 里有索引,就放心了。
但实际上:
“走索引”和“查询快”不是一回事。
走索引,只能说明数据库觉得“从这个入口进”更合适,
但进去了以后,是不是还要扫描一大堆数据、回表很多次、排序很多次,那是另一回事。
1. 区分度太低的索引,价值可能没你想的那么大
比如字段:
status只有几个值:
- 0:待处理
- 1:已完成
- 2:已取消
这时候你给 status 单独建索引,看起来没毛病。
但如果一张表有 500 万数据,其中 300 万都是 status = 1,那你查:
SELECT *FROM ordersWHERE status = 1;即使走了索引,也可能要扫出大量记录。
这种情况下,索引的过滤能力其实很有限。
所以索引好不好,不是看“有没有”,而是看它能不能快速缩小结果集。
一句话说透:
索引最怕的不是没有,而是“有了跟没有差不多”。
2. select * + 回表,是很多慢查询的老毛病
来看一个典型场景:
SELECT *FROM ordersWHERE user_id = 10001ORDER BY create_time DESCLIMIT 20;你可能已经给 user_id 建了索引,甚至也走了索引。
但为什么还慢?
因为你查的是 *。
如果索引里没有你需要的全部字段,数据库通常要先从索引里找到主键,再回到主表里把整行数据取出来,这就是常说的回表。
当返回数据很多,或者扫描候选行很多时,回表成本就会上来。
这也是为什么“覆盖索引”很重要
比如你真正只需要这些字段:
SELECT id, user_id, create_time, amountFROM ordersWHERE user_id = 10001ORDER BY create_time DESCLIMIT 20;然后索引设计成:
INDEX idx_user_time_amount (user_id, create_time, amount)这时很多数据库就有机会直接从索引里把结果拿出来,减少回表成本。
不是说所有 SQL 都必须追求覆盖索引,
但至少你要知道:大量 select *,本身就是性能优化的敌人。
3. 深分页,才是很多“明明走索引还慢”的真凶
再看一个业务里很常见的写法:
SELECT id, title, create_timeFROM articleORDER BY create_time DESCLIMIT 100000, 20;这个 SQL 很多人第一眼会想:
“不是有索引吗?为什么还慢?”
问题在于 LIMIT 100000, 20 这种深分页,本质上不是直接跳到第 100001 条,而是前面的大量数据仍然要参与定位、排序、跳过。
你看到的是取 20 条,
数据库干的却可能是:先处理前面 100020 条,再丢掉其中 100000 条。
这时候瓶颈根本不在“有没有索引”,而在分页方式。
更适合大数据量场景的,是基于游标/上次位置继续翻页,例如:
SELECT id, title, create_timeFROM articleWHERE create_time < '2026-03-06 12:00:00'ORDER BY create_time DESCLIMIT 20;这类方式通常比深分页更稳。
三、你以为是索引慢,实际上慢在索引外面
这是我觉得最值得警惕的一点。
很多时候,大家把问题都集中在 where 条件和索引上,
但真实的慢点,可能压根不在那里。
数据库查询慢,除了“找数据”,还有很多别的成本:
- 排序
- 临时表
- 分组
- 回表
- 锁等待
- 网络传输
- 应用层拿太多数据
- 统计信息不准导致执行计划选错
所以性能分析如果只停留在“加索引”,往往会越改越偏。
1. 排序没走上索引,可能比过滤更慢
例如:
SELECT *FROM ordersWHERE user_id = 10001ORDER BY update_time DESC;你可能给 user_id 建了索引,也确实过滤变快了。
但如果 ORDER BY update_time 没法和索引顺序匹配,数据库可能还得额外排序。
真正慢的,不一定是 where,
而是后面的 filesort。
所以很多 SQL 优化,不是“多加一个索引字段”那么简单,
而是要整体看:
- where 怎么过滤
- order by 怎么排序
- limit 怎么截取
- 返回列有哪些
它本质上是一个完整执行路径的问题。
2. 锁等待,看起来像慢查询,其实不是查得慢
业务系统里还有一种情况特别坑:
SQL 本身不复杂,执行计划也不差,但它就是慢。
这时候你再去看数据库监控,发现可能不是 CPU 高,不是扫描行多,而是被锁住了。
比如:
- 一个大事务长时间不提交
- 更新和查询互相等待
- 热点行频繁修改
- 批量操作把锁持有时间拖太长
这类场景下,SQL 慢只是表象,真正慢的是等待。
也就是说:
你以为在做索引优化,实际上应该先做事务治理。
3. 执行计划不是一成不变的,数据量一变,原来的“好索引”也可能变味
很多开发有个误区:
“我上个月看过这个 SQL 的执行计划,是走索引的,没问题。”
但数据库不是静态的。
随着数据量变化、分布变化、冷热数据变化,优化器的选择也可能变。
今天它觉得索引最优,明天它可能觉得全表扫更划算。
所以真正成熟的性能优化,不是“建完索引就结束”,而是:
- 看执行计划
- 看扫描行数
- 看实际返回行数
- 看慢日志
- 看锁等待
- 看数据分布是否变化
说白了,索引优化不是一次性动作,而是持续校准。
该怎么真正判断“为什么加了索引还慢”?
我自己更建议按下面这个顺序排查,而不是一上来就盲加索引。
第一步:先看 SQL 是否真的用上了索引
看 EXPLAIN,重点关注:
- type
- key
- rows
- Extra
尤其注意有没有这些信号:
- Using where
- Using filesort
- Using temporary
如果索引都没选上,先别谈优化效果。
第二步:看“用了索引以后扫了多少”
很多 SQL 最大的问题不是“没走索引”,
而是“走了个没什么过滤能力的索引”。
这时候要看:
- 扫描行数大不大
- 返回行数多不多
- 是否低区分度字段顶在前面
- 是否存在大量回表
第三步:看慢点是不是在排序、分页、锁等待
不要把所有问题都归因到索引。
真正成熟的排查一定会继续看:
- 是否深分页
- 是否排序没命中索引
- 是否临时表/分组过重
- 是否事务过大
- 是否有锁竞争
一个很实用的结论
我越来越觉得,数据库优化里最容易出问题的一点就是:
大家总在问“有没有索引”,却很少认真问“数据库为了这条 SQL 到底做了什么”。
索引只是入口。
而一次查询真正耗时的地方,可能在入口之后很远。
所以,“加了索引为什么还慢”,很多时候无非就这 3 类原因:
1. 索引没真正用对
函数、隐式转换、联合索引使用方式不对,都会让索引效果大打折扣。
2. 走了索引,但扫描和回表成本依然很高
低区分度、select *、深分页,都会让“有索引”变得意义有限。
3. 真正的慢点不在索引本身
排序、锁等待、临时表、执行计划变化,才是很多 SQL 变慢的根源。
最后
索引从来不是“加上就快”的开关,
它更像是一套交通规则。
你路修了,不代表车就一定跑得快;
方向不对、路口堵住、前面排队、出口设计有问题,照样快不起来。
所以做数据库优化,别只盯着“建索引”这一个动作。
真正重要的是:
看清楚这条 SQL 是怎么走的,慢到底慢在哪一段。
这才是从“会加索引”,走向“会做性能优化”的分水岭。
如果这篇内容对你有帮助,欢迎点赞 、收藏 ⭐、转发给需要的朋友
我会持续分享:
- ☕ Java 核心与高阶实战
- AI / Agent / 前沿技术落地
- 真实项目经验 & 架构思考
- ️ 企业数字化与产品实践
关注我,一起把“技术”真正用在项目和业务里。
你的每一次支持,都是我持续输出高质量内容的最大动力。