很多人遇到 SQL 慢查询时,第一反应是:“这条 SQL 明明建了索引,为什么 MySQL 还是走全表扫描?”
于是就开始补索引、加联合索引、FORCE INDEX,甚至看到 EXPLAIN 里的 type=ALL 就认定“索引失效了”。
但真正做过数据库性能优化之后会发现,所谓“索引失效”其实不是一个单一问题。
有时候是 SQL 写法让优化器无法有效利用索引;有时候是联合索引设计不合理;有时候索引其实用了,只是扫描范围太大,使用索引反而比全表扫描更贵;还有一些场景,本身就不适合依赖 B+Tree 索引解决。
所以,排查索引问题最重要的并不是背诵“索引失效的 10 种情况”,而是建立一套稳定的分析路径:
先确认执行计划 → 再定位索引为什么没有产生收益 → 能改 SQL 就改 SQL → 不能改 SQL 就改索引/表结构 → 仍然无解时,往架构层处理。
本文就沿着这条思路,把 MySQL 索引问题完整梳理一遍。
一、先别急着说“索引失效”,先看执行计划
假设有一张订单表:
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
status TINYINT NOT NULL,
created_at DATETIME NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
KEY idx_user_created (user_id, created_at),
KEY idx_status_created (status, created_at)
);
我们执行:
SELECT *
FROM orders
WHERE user_id = 10001
AND created_at >= '2026-09-01';
先不要猜,直接:
EXPLAIN SELECT *
FROM orders
WHERE user_id = 10001
AND created_at >= '2026-09-01';
重点观察几个字段:
possible_keys:理论上哪些索引可能被使用。key:最终优化器选择了哪个索引。key_len:实际使用了联合索引的多少部分。type:访问方式,例如const、ref、range、index、ALL。rows:优化器估算需要扫描多少行。filtered:经过条件过滤后预计还剩多少比例。Extra:是否出现Using index、Using where、Using filesort、Using temporary等信息。
这里有一个非常容易踩的误区:
key不是NULL,并不代表查询就一定很快;type不是ALL,也不代表索引使用得很好。
例如一个低选择性的索引可能只过滤掉很少的数据。此时优化器判断全表扫描成本更低,是完全合理的。
MySQL 官方也建议使用 EXPLAIN 来检查执行计划;对于需要进一步确认“优化器估算值”和“真实执行情况”是否一致的场景,可以使用 EXPLAIN ANALYZE。urlMySQL EXPLAIN 官方文档https://dev.mysql.com/doc/refman/8.4/en/using-explain.html
二、最常见的问题:对索引列做函数或表达式计算
例如有索引:
KEY idx_created_at (created_at)
但查询写成:
SELECT *
FROM orders
WHERE DATE(created_at) = '2026-09-09';
问题在于:MySQL 需要先计算 DATE(created_at),再判断结果是不是目标日期。
这和直接在索引列上做范围查询完全不同:
SELECT *
FROM orders
WHERE created_at >= '2026-09-09 00:00:00'
AND created_at < '2026-09-10 00:00:00';
第二种写法可以直接把条件转换为索引范围。
类似的问题还有:
WHERE YEAR(created_at) = 2026
WHERE ABS(amount) > 100
WHERE LOWER(name) = 'samoy'
WHERE CAST(user_id AS CHAR) = '10001'
核心原则可以概括成一句话:
尽量让索引列保持“裸奔”,不要在索引列上再套函数、运算或转换。
不是所有函数都一定没办法
如果业务上就是必须查询计算后的值,可以考虑把表达式变成可索引的数据。
例如 MySQL 支持生成列并为生成列建立索引:
ALTER TABLE users
ADD COLUMN name_lower VARCHAR(255)
GENERATED ALWAYS AS (LOWER(name)) STORED,
ADD INDEX idx_name_lower (name_lower);
这样查询可以改成:
SELECT *
FROM users
WHERE name_lower = 'samoy';
这类方案的本质不是“强行让原来的 SQL 走索引”,而是把查询条件提前物化成一个真正可以建立索引的值。MySQL 官方文档也明确支持对 generated column 建立索引。urlMySQL Generated Column Index 官方文档https://dev.mysql.com/doc/refman/8.4/en/generated-column-index-optimizations.html
三、隐式类型转换:看起来一样,实际上类型不一样
这是线上非常常见的一类问题。
例如:
CREATE TABLE users (
id BIGINT PRIMARY KEY,
phone VARCHAR(20),
KEY idx_phone (phone)
);
如果查询:
SELECT *
FROM users
WHERE phone = 13800138000;
数据库字段是字符串,但传入的参数是数字。此时涉及类型转换,执行计划可能和你预期的不一样。
正确做法是保证参数类型和字段类型一致:
SELECT *
FROM users
WHERE phone = '13800138000';
在 Java、Spring Boot、MyBatis 这类项目里尤其要注意:不要只看 SQL 模板,还要看最终绑定参数的类型。
很多“数据库明明有索引,但线上就是不走”的问题,最后都是在参数类型上找到原因。
四、LIKE 并不是天然不能走索引
经常有人说:
“
LIKE会导致索引失效。”
这句话并不准确。
例如:
WHERE name LIKE 'Sam%'
这种“前缀匹配”在合适的索引和字符集条件下仍然可以使用 B+Tree 索引。
真正麻烦的是:
WHERE name LIKE '%Sam'
以及:
WHERE name LIKE '%Sam%'
因为前面有 %,数据库无法直接从索引树上确定一个连续的起始范围。
这种情况怎么优化?
先问自己一个问题:
我真正需要的是“前缀匹配”,还是“全文包含匹配”?
如果业务只是前缀搜索,把 SQL 改成:
WHERE name LIKE 'Sam%'
就可能已经解决问题。
但如果业务真的需要“任意位置包含”,那就别再执着于 B+Tree 索引。可以根据场景考虑:
- MySQL FULLTEXT 全文索引;
- 专门的搜索引擎,例如 Elasticsearch;
- 业务侧拆分搜索字段;
- 维护额外的倒排数据结构。
这也是本文后面要强调的一件事:
不是所有查询都应该靠普通索引解决。
五、联合索引最容易踩的坑:最左匹配不是“背口诀”那么简单
假设有联合索引:
KEY idx_user_status_created (user_id, status, created_at)
那么下面几个查询的索引利用方式并不一样:
-- 很理想
WHERE user_id = 10001
AND status = 1
AND created_at >= '2026-09-01'
-- 仍然可以很好利用索引
WHERE user_id = 10001
AND created_at >= '2026-09-01'
-- 少了最左列,通常无法按照这个联合索引直接定位 user_id 范围
WHERE status = 1
AND created_at >= '2026-09-01'
很多文章把它简单总结成“联合索引必须遵循最左匹配原则”,这句话没错,但还不够。
真正应该理解的是:
B+Tree 联合索引本质上是按照
(user_id, status, created_at)的顺序排序的。
因此 MySQL 能不能高效地缩小搜索范围,取决于查询条件能不能沿着这个排序顺序建立一个连续范围。
还有一个经常被误解的问题:范围条件之后的列到底还能不能用?
例如:
KEY idx_a_b_c (a, b, c)
查询:
WHERE a = 10
AND b > 20
AND c = 30
不能简单理解成“c 就完全没用了”。更准确的说法是:b > 20 已经把索引扫描范围扩大成一个区间,后面的 c 往往不能再像等值条件那样继续缩小 B+Tree 的查找边界,但仍可能参与其他优化,例如索引条件下推等。
所以分析联合索引时,不要只看“有没有命中”,而要看:到底利用了索引的哪一部分,以及最终扫描了多少行。
六、OR、!=、NOT IN:不是绝对失效,而是可能“不划算”
1. OR
比如:
WHERE user_id = 10001 OR status = 1
这不意味着一定不用索引。MySQL 在某些场景下可以使用 Index Merge 等策略。
但如果两个条件的选择性都很差,或者数据量很大,优化器判断直接扫表更便宜,也是正常的。
2. !=
WHERE status != 0
假设 95% 的数据 status 都是 1,那么这个条件本身就没有什么筛选能力。
就算有索引,扫描索引后仍然需要访问大量数据,成本可能高于全表扫描。
3. NOT IN
同理:
WHERE status NOT IN (1, 2)
真正需要关心的是查询结果的选择性,而不是“语法长什么样”。
所以看到这些 SQL 时,不应该下结论:
“因为用了
!=,所以索引失效。”
正确的分析应该是:
“这个条件到底能过滤掉多少数据?使用索引后需要回表多少次?整体成本是否真的比全表扫描低?”
七、索引没走,还有一种可能:优化器认为“不值得走”
这是最容易被忽略的一层。
假设:
SELECT *
FROM orders
WHERE status = 1;
你建了:
KEY idx_status (status)
但如果表里 90% 的记录 status = 1,这个索引的选择性就很低。
此时如果走索引:
- 先扫描大量二级索引记录;
- 再根据主键回表;
- 最后拿到绝大部分数据。
还不如直接把整张表顺序扫一遍。
所以:
“不走索引”不一定是数据库出了问题,有时恰恰说明优化器认为全表扫描更便宜。
怎么验证是不是统计信息的问题?
如果你非常确定某个索引应该有明显收益,却发现优化器长期选择了一个奇怪的执行计划,可以先更新统计信息:
ANALYZE TABLE orders;
MySQL 官方文档也建议,当索引没有按预期被使用时,可以通过 ANALYZE TABLE 更新表统计信息。urlMySQL EXPLAIN 官方文档https://dev.mysql.com/doc/refman/8.4/en/explain.html
然后重新 EXPLAIN 看执行计划有没有变化。
八、ORDER BY / GROUP BY 也会让“有索引”和“高性能”变成两回事
例如:
SELECT *
FROM orders
WHERE user_id = 10001
ORDER BY amount DESC;
即使 user_id 有索引,也不意味着查询就可以顺便利用该索引完成排序。
如果索引顺序与过滤条件、排序条件不匹配,最终仍可能出现:
Using filesort
同样,Using filesort 也不等于“索引失效”。
它表达的是:排序没有完全依赖索引顺序完成。
有时这是合理且不可避免的;真正需要关心的是排序的数据量有多大、临时结构是否巨大、整体耗时是否可接受。
因此,一个更合理的联合索引可能是:
KEY idx_user_amount (user_id, amount)
然后让查询尽可能沿着索引顺序直接读取。
这里依然不要死记“看到 Using filesort 就加索引”,而应该回到执行计划和实际数据量。
九、为什么“加一个索引”往往不是最好的优化方式?
因为索引不是免费的。
每增加一个索引,就意味着:
- 占用额外磁盘空间;
- 增加 Buffer Pool 的内存压力;
INSERT需要维护更多索引;UPDATE可能维护更多索引;DELETE也需要同步更新索引。
所以索引优化的目标不是:
“尽可能多地建索引。”
而应该是:
“用尽可能少的索引覆盖尽可能重要的查询模式。”
MySQL 官方同样提醒,不必要的索引会浪费空间,并增加写操作维护成本,需要在查询性能和索引成本之间取得平衡。urlMySQL Optimization and Indexes 官方文档https://dev.mysql.com/doc/refman/8.4/en/optimization-indexes.html
十、真正实战时,我会按照这个顺序排查
遇到一条慢 SQL,我一般不会上来就修改索引,而是按照下面的路径走。
第一步:确认是不是 SQL 本身慢
记录真实 SQL、参数、执行时间,不要拿一个脱离真实数据的例子分析。
第二步:看 EXPLAIN
重点确认:
key
key_len
rows
filtered
Extra
看“用了哪个索引”只是第一层,更重要的是:到底扫描了多少行。
第三步:看真实执行情况
对于支持的 MySQL 版本,可以进一步使用:
EXPLAIN ANALYZE
SELECT ...;
它会提供实际执行耗时、实际返回行数和循环次数等信息,可以帮助判断优化器的估算是否偏离真实情况。urlMySQL EXPLAIN ANALYZE 官方文档https://dev.mysql.com/doc/refman/8.4/en/explain.html
第四步:检查 SQL 有没有破坏索引使用条件
重点查:
索引列函数
隐式类型转换
LIKE '%xxx%'
复杂 OR
不必要的表达式
第五步:检查联合索引顺序
把查询中的条件拆成:
等值条件 → 范围条件 → 排序/分组 → 回表字段
再重新设计索引。
第六步:检查数据分布
重点看:
索引选择性
数据量
热点值
NULL/默认值分布
不要只根据字段“看起来应该建索引”来设计。
第七步:最后才考虑强制优化器选索引
MySQL 提供 index hint,例如:
SELECT *
FROM orders FORCE INDEX (idx_user_created)
WHERE user_id = 10001;
但我非常不建议把 FORCE INDEX 当作第一选择。
因为今天的数据分布可能让这个索引最优,半年后数据分布发生变化,原本正确的 hint 可能反而把优化器锁死在一个更差的执行计划上。
MySQL 8.4 还提供了更细粒度的 optimizer hints,可以针对具体语句控制优化器行为。urlMySQL Optimizer Hints 官方文档https://dev.mysql.com/doc/refman/8.4/en/optimizer-hints.html
十一、索引真的无法解决时,怎么办?
这是我认为比“索引失效 10 种情况”更值得掌握的一部分。
因为有些查询,从根上就不适合继续堆索引。
场景一:搜索条件本身就是全文匹配
例如:
WHERE content LIKE '%mysql%'
如果数据量已经非常大,再怎么折腾普通 B+Tree,也很难让它变成高效的全文搜索。
此时应该考虑 FULLTEXT 或专门的搜索系统。
场景二:查询本身就需要扫描大量数据
比如:
SELECT SUM(amount)
FROM orders
WHERE created_at >= '2020-01-01';
如果命中的就是全表 80% 的数据,索引不一定能带来决定性收益。
这时候可以考虑:
- 预聚合;
- 汇总表;
- 按天/月维护统计数据;
- 缓存热门统计结果。
例如把每天的订单金额提前汇总到:
order_daily_stat
----------------
date
order_count
total_amount
查询就从“扫描海量明细”变成“扫描少量统计数据”。
这已经不是索引优化,而是改变数据访问模型。
场景三:分页越来越慢
很多系统都会写成:
SELECT *
FROM orders
ORDER BY id DESC
LIMIT 100000, 20;
即使 id 有索引,也意味着数据库需要跳过前面的大量记录。
这时可以改成基于游标/范围的分页:
SELECT *
FROM orders
WHERE id < 123456
ORDER BY id DESC
LIMIT 20;
这种方案的收益通常不是“多建了一个索引”,而是减少了数据库必须扫描的数据范围。
场景四:单表已经巨大
如果一张业务表已经达到非常大的规模,单纯继续优化索引可能收益越来越有限。
此时可以根据业务访问模式考虑:
- 分区表;
- 冷热数据分离;
- 历史数据归档;
- 读写分离;
- 分库分表。
但要注意:分区、分库分表都不是“索引失效后的万能药”。
它们解决的是数据规模和访问路径的问题,复杂度和运维成本也会显著上升。
十二、还有一个经常被忽略的方向:减少回表
假设:
SELECT user_id, created_at
FROM orders
WHERE user_id = 10001
AND created_at >= '2026-09-01';
如果索引本身已经包含:
KEY idx_user_created (user_id, created_at)
那么查询所需要的字段都在索引里,MySQL 在合适的情况下可以直接从索引得到结果,而不必再回表读取整行数据。
这就是常说的覆盖索引。
可以通过 EXPLAIN 观察是否出现:
Using index
覆盖索引的价值往往比单纯讨论“走不走索引”更实际:
索引不只是用来定位数据,还可以直接承载查询需要的数据。
当然,也不能为了覆盖一个查询就无节制地建立“巨宽索引”,因为索引越宽,存储和写放大成本越高。
十三、不要迷信“看到全表扫描就是坏事”
最后再强调一次:
type = ALL
并不等于数据库一定有问题。
如果一张表只有几百行,甚至几十行:
SELECT * FROM config WHERE status = 1;
直接扫表可能就是最合理的方案。
同样:
Using filesort
也不等于必须加索引。
possible_keys 有值也不代表一定应该使用;key 有值也不代表一定比全表扫描快。
真正需要关注的是成本,而不是某一个 EXPLAIN 字段是否“好看”。
十四、我对“索引失效”的理解
做数据库性能优化时,我越来越不喜欢“索引失效”这个词。
因为它很容易让人陷入一种错误思维:
“索引没用上,所以我要想办法让它用上。”
但正确的问题应该是:
“为什么当前执行计划成本更高?我要怎么让数据库更少地扫描、更少地回表、更少地排序、更少地处理无关数据?”
于是整个优化过程其实就变成了一个漏斗:
慢 SQL
↓
EXPLAIN / EXPLAIN ANALYZE
↓
扫描行数是否过多?
↓
SQL 写法有问题? ──→ 改 SQL
↓
索引设计有问题? ──→ 改索引
↓
统计信息有问题? ──→ ANALYZE TABLE
↓
索引仍然收益有限? ──→ 覆盖索引 / 生成列 / 重构查询
↓
查询本身就要处理海量数据?
↓
缓存 / 汇总表 / 归档 / 分区 / 搜索引擎 / 读写分离
到最后你会发现:
数据库优化的终点,从来不是“让 SQL 走索引”,而是“让系统少做无意义的工作”。
索引只是其中一个非常重要的工具,但绝不是唯一的工具。
参考资料
- MySQL 8.4 Reference Manual — Optimization and Indexes
- MySQL 8.4 Reference Manual — Optimizing Queries with EXPLAIN
- MySQL 8.4 Reference Manual — EXPLAIN Statement
- MySQL 8.4 Reference Manual — Generated Column Indexes
- MySQL 8.4 Reference Manual — Optimizer Hints
- MySQL 8.4 Reference Manual — Invisible Indexes
欢迎在评论区留下您的见解~