数据库索引失效的常见场景与排查:从执行计划入手

举报
yd_236134918 发表于 2026/11/08 06:34:49 2026/11/08
【摘要】 建了索引查询还是慢?本文结合订单表场景,讲清函数运算、隐式类型转换、LIKE 前导通配符、OR 条件、否定条件、最左前缀失效等七种常见索引失效场景,并给出可复用的 EXPLAIN 排查流程。

建了索引,查询还是慢——很多时候不是索引没用,而是查询写法让索引“失效”了。本文结合后端接口里最常见的订单表场景,讲清楚索引失效的几种典型情况,以及一套可以照着做的排查流程。

先说结论:失效不等于没建索引

数据库优化器会根据统计信息、选择度、成本来决定走不走索引。同一条 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;

一套可复用的排查流程

  1. 先复现:拿到慢查询的完整 SQL 和参数,不要只看接口耗时。
  2. 看 EXPLAIN:确认 type、key、rows、Extra(出现 Using filesort、Using temporary 也要留心)。
  3. 对照表结构:字段类型、索引定义、联合索引顺序是否和查询一致。
  4. 逐条对照上面七个场景:函数、类型转换、LIKE、OR、否定条件、最左前缀、选择度。
  5. 改写 SQL 再测一次:确认 key 变了、rows 明显下降,再上线。
  6. 定期复查:数据量和数据分布会变,曾经合理的索引可能慢慢失效,慢查询日志值得每周看一次。

结语

索引失效大多不是数据库的问题,而是查询写法和索引设计没有对齐。养成两个习惯就能避开大部分坑:写 SQL 时先想“这个条件能不能直接定位到索引”,上线前用 EXPLAIN 看一眼。把这两步做进日常的接口开发和 Code Review 里,慢查询会少很多。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。