数据库索引失效的常见场景与排查:从执行计划入手
建了索引,查询还是慢——很多时候不是索引没用,而是查询写法让索引“失效”了。本文结合后端接口里最常见的订单表场景,讲清楚索引失效的几种典型情况,以及一套可以照着做的排查流程。
先说结论:失效不等于没建索引
数据库优化器会根据统计信息、选择度、成本来决定走不走索引。同一条 SQL,在数据量小、表只有几百行时走全表扫描也可能是合理的;到了几十万行,同样的写法就可能成为慢查询的源头。所以排查的第一步永远是看执行计划,而不是凭感觉加索引。
EXPLAIN SELECT * FROM orders WHERE user_id = 1001 AND status = 1;
重点看三个字段:type(ref、range 是好信号,ALL 基本就是全表扫描)、key(实际用到的索引,没有就是 NULL)、rows(预估扫描行数)。如果 key 是 NULL 而你明明建了索引,再往下对照后面的场景逐一排查。
场景一:对索引列做了函数或运算
-- 失效:对 create_time 做了函数,索引无法直接定位
SELECT * FROM orders WHERE DATE(create_time) = '2026-11-08';
-- 改写:改成范围条件,索引可以正常使用
SELECT * FROM orders WHERE create_time >= '2026-11-08' AND create_time < '2026-11-09';
同理还有 YEAR(create_time)、CONCAT、对数值列做 amount + 0、amount * 100 这类写法。原则很简单:让索引列“干净”地出现在比较的一侧,需要转换的放到另一侧,或者用范围条件替代函数。
场景二:隐式类型转换
这是线上最容易踩、也最隐蔽的一种。user_id 是 VARCHAR,查询却传了数字:
-- user_id 是 VARCHAR,传入数字会触发隐式转换,索引失效
SELECT * FROM orders WHERE user_id = 1001;
-- 改写:按字段类型传参
SELECT * FROM orders WHERE user_id = '1001';
后端接口里对应的坑是参数类型不一致:接口收到的是字符串,ORM 或手写 SQL 拼成了数字;或者反过来,字段是 BIGINT,却用字符串去查。排查时看 EXPLAIN 的 key 为 NULL、同时 type 是 ALL,基本可以往这个方向想。
场景三:LIKE 以通配符开头
-- 失效:前导 % 让 B+ 树无法从左向右匹配
SELECT * FROM orders WHERE order_no LIKE '%20261108%';
-- 可走索引:前缀固定时可以走索引范围扫描
SELECT * FROM orders WHERE order_no LIKE '20261108%';
如果业务确实需要模糊匹配,考虑全文索引、搜索引擎,或者把可枚举的维度拆成单独字段做精确查询,而不是让核心查询长期依赖 %xxx%。
场景四:OR 条件两侧没有同时用到索引
-- user_id 有索引,mobile 没有索引:OR 会让优化器放弃索引
SELECT * FROM orders WHERE user_id = '1001' OR mobile = '13800000000';
常见改法有两种:一是给另一侧也补上合适的索引;二是改写成 UNION(两个分支各自走索引再合并去重)。但要注意,UNION 并不总是更优,具体要以执行计划和实测为准,不要凭经验一刀切。
场景五:不等、否定与 NULL 判断
!=、<>、NOT IN、NOT EXISTS、IS NOT NULL 这类条件通常难以走索引,因为它们要排除的范围太大,优化器算下来全表扫描反而成本更低。遇到这类查询,先问一句:能不能改成正向条件?比如把“状态不等于已取消”改成“状态属于待支付、已支付”,用 IN 列举出实际要查的状态。
另外注意,索引列上的 NULL 值在某些存储引擎和索引类型下不会被索引覆盖,IS NULL 能走、IS NOT NULL 不一定能走,具体看执行计划。
场景六:违反最左前缀原则
假设有联合索引 (user_id, status, create_time):
-- 只查 status:跳过了最左列,索引用不上
SELECT * FROM orders WHERE status = 1;
-- 只查 user_id、status:能用到前两列
SELECT * FROM orders WHERE user_id = '1001' AND status = 1;
-- 跳过 status 查 create_time:只能用到 user_id 这一列
SELECT * FROM orders WHERE user_id = '1001' AND create_time >= '2026-11-01';
联合索引的列顺序要按“区分度高、查询最常用”的顺序排,并且要留心范围条件后面的列会失效:user_id = ? AND create_time >= ? AND status = ? 里,status 这一列就用不上索引了,因为范围条件截断了后面的匹配。
场景七:优化器认为全表扫描更便宜
索引不是越多越好。当过滤条件区分度很低时(比如 status 只有 0 和 1 两种值,几乎一半数据都满足),走索引要回表一次次随机读,反而不如顺序全表扫描。EXPLAIN 里 key 为 NULL、rows 接近全表行数时,先别急着加索引,看看是不是该换个区分度更高的条件、或者建覆盖索引避免回表。
覆盖索引的思路是:让查询只用索引列就能返回结果。
-- 如果 (user_id, status, id) 是覆盖索引,这个查询不需要回表
SELECT id FROM orders WHERE user_id = '1001' AND status = 1;
一套可复用的排查流程
- 先复现:拿到慢查询的完整 SQL 和参数,不要只看接口耗时。
- 看
EXPLAIN:确认type、key、rows、Extra(出现Using filesort、Using temporary也要留心)。 - 对照表结构:字段类型、索引定义、联合索引顺序是否和查询一致。
- 逐条对照上面七个场景:函数、类型转换、LIKE、OR、否定条件、最左前缀、选择度。
- 改写 SQL 再测一次:确认
key变了、rows明显下降,再上线。 - 定期复查:数据量和数据分布会变,曾经合理的索引可能慢慢失效,慢查询日志值得每周看一次。
结语
索引失效大多不是数据库的问题,而是查询写法和索引设计没有对齐。养成两个习惯就能避开大部分坑:写 SQL 时先想“这个条件能不能直接定位到索引”,上线前用 EXPLAIN 看一眼。把这两步做进日常的接口开发和 Code Review 里,慢查询会少很多。
- 点赞
- 收藏
- 关注作者
评论(0)