MySQL索引明明建了,查询还是慢,排查发现踩了这些坑

背景

同事说他的SQL查询很慢,但他明明建了索引。

我过去一看:

ini 复制代码
SELECT * FROM orders WHERE user_id = 10086 AND status = 1;

表有500万数据,user_idstatus都有索引,但这条SQL执行要3秒。

EXPLAIN一看:

sql 复制代码
EXPLAIN SELECT * FROM orders WHERE user_id = 10086 AND status = 1;
+----+-------------+--------+------+---------------+------+---------+------+---------+-------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows    | Extra       |
+----+-------------+--------+------+---------------+------+---------+------+---------+-------------+
|  1 | SIMPLE      | orders | ALL  | idx_user_id   | NULL | NULL    | NULL | 5000000 | Using where |
+----+-------------+--------+------+---------------+------+---------+------+---------+-------------+

type=ALL,全表扫描,索引根本没用上。

为什么?总结了索引失效的常见原因。

一、对索引列做运算或函数

错误示例

sql 复制代码
-- 对索引列使用函数
SELECT * FROM users WHERE YEAR(create_time) = 2024;

-- 对索引列做运算
SELECT * FROM orders WHERE order_id + 1 = 10087;

原因

B+树索引存的是列的原始值,你用函数或运算处理后,MySQL没法直接用索引查找。

正确写法

sql 复制代码
-- 改成范围查询
SELECT * FROM users WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';

-- 把运算移到右边
SELECT * FROM orders WHERE order_id = 10087 - 1;

二、隐式类型转换

错误示例

sql 复制代码
-- phone是VARCHAR类型
SELECT * FROM users WHERE phone = 13800138000;

phone是字符串,但传入的是数字,MySQL会做隐式转换。

EXPLAIN结果

yaml 复制代码
type: ALL
key: NULL

索引失效了。

原因

MySQL的转换规则是:字符串转数字,而不是数字转字符串。

所以实际执行的是:

sql 复制代码
SELECT * FROM users WHERE CAST(phone AS SIGNED) = 13800138000;

等于对索引列做了函数处理,索引失效。

正确写法

ini 复制代码
SELECT * FROM users WHERE phone = '13800138000';

记住:类型要匹配,字符串就传字符串。

三、LIKE以%开头

错误示例

sql 复制代码
SELECT * FROM products WHERE name LIKE '%手机%';

原因

B+树索引是按顺序排列的,%手机%没法利用索引定位,只能全表扫描。

可以用索引的写法

sql 复制代码
-- 前缀匹配可以用索引
SELECT * FROM products WHERE name LIKE '手机%';

全文搜索怎么办

如果业务需要中间匹配:

  1. 全文索引:MySQL 5.7+支持中文全文索引
  2. Elasticsearch:专业的搜索引擎
  3. 搜索优化:用其他条件先过滤,再LIKE

四、OR条件只有部分有索引

错误示例

sql 复制代码
-- user_id有索引,remark没索引
SELECT * FROM orders WHERE user_id = 10086 OR remark = '测试';

EXPLAIN结果

bash 复制代码
type: ALL

全表扫描。

原因

OR的两个条件,只要有一个没索引,就没法用索引。

正确写法

sql 复制代码
-- 方案1:给remark也加索引

-- 方案2:改成UNION
SELECT * FROM orders WHERE user_id = 10086
UNION
SELECT * FROM orders WHERE remark = '测试';

五、联合索引没遵循最左前缀

假设有联合索引

css 复制代码
CREATE INDEX idx_abc ON orders(a, b, c);

能用上索引的查询

ini 复制代码
WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3
WHERE a = 1 AND c = 3  -- 只能用到a

用不上索引的查询

ini 复制代码
WHERE b = 2           -- 没有a,最左前缀断了
WHERE b = 2 AND c = 3 -- 没有a
WHERE c = 3           -- 没有a

原理

联合索引的B+树是按(a, b, c)顺序排列的,先按a排序,a相同的按b排序,b相同的按c排序。

如果查询不包含a,就没法利用这个排序结构。

六、范围查询后的列失效

联合索引

css 复制代码
CREATE INDEX idx_abc ON orders(a, b, c);

查询

css 复制代码
SELECT * FROM orders WHERE a = 1 AND b > 10 AND c = 3;

实际用到的索引

只用到了abc没用上。

原因

范围查询(>、<、BETWEEN、LIKE)会导致后面的列无法使用索引。

因为在b > 10的范围内,c的值不是有序的。

优化建议

把等值查询的列放前面,范围查询的列放后面:

css 复制代码
CREATE INDEX idx_acb ON orders(a, c, b);

七、NOT IN和NOT EXISTS

示例

sql 复制代码
SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM blacklist);

情况

  • IN通常可以用索引
  • NOT IN某些情况会导致全表扫描

优化

sql 复制代码
-- 用LEFT JOIN替代
SELECT u.* FROM users u 
LEFT JOIN blacklist b ON u.id = b.user_id 
WHERE b.user_id IS NULL;

八、使用!=或<>

示例

sql 复制代码
SELECT * FROM orders WHERE status != 1;

情况

不等于查询有时候会导致索引失效,取决于数据分布。

如果status != 1的数据占大多数,MySQL可能认为全表扫描更快。

建议

用EXPLAIN看实际执行计划,如果数据分布合适,可以改成:

sql 复制代码
SELECT * FROM orders WHERE status IN (0, 2, 3, 4);

九、IS NULL的情况

老版本MySQL

IS NULL可能导致索引失效。

MySQL 5.7+

IS NULL可以使用索引:

sql 复制代码
SELECT * FROM users WHERE phone IS NULL;  -- 可以用索引

建议

  • 尽量不要用NULL,用默认值代替
  • 如果必须用NULL,确保MySQL版本较新

十、ORDER BY没用上索引

联合索引

css 复制代码
CREATE INDEX idx_abc ON orders(a, b, c);

能用索引排序的

css 复制代码
ORDER BY a
ORDER BY a, b
ORDER BY a, b, c
ORDER BY a DESC, b DESC, c DESC  -- 方向一致

不能用索引排序的

css 复制代码
ORDER BY b                -- 没有a
ORDER BY a ASC, b DESC    -- 方向不一致
ORDER BY a, c             -- 跳过了b

排查索引问题的流程

  1. EXPLAIN看执行计划
  • type:ALL是全表扫描,ref/range/const是用了索引

  • key:实际使用的索引

  • rows:预估扫描行数

  1. 看possible_keys和key
  • possible_keys有值但key是NULL:索引存在但没用上
  1. 开启optimizer_trace
    SET optimizer_trace = 'enabled=on'; SELECT * FROM orders WHERE ...; SELECT * FROM information_schema.optimizer_trace\G
    可以看到MySQL为什么选择了某个执行计划。

总结

索引失效的常见原因:

原因 示例 解决方案
对索引列做函数 WHERE YEAR(date)=2024 改成范围查询
隐式类型转换 WHERE phone=138xxx(数字) 类型匹配
LIKE %开头 WHERE name LIKE '%xxx' 改前缀或用ES
OR部分无索引 WHERE a=1 OR b=2 都加索引或UNION
最左前缀 联合索引(a,b,c)查WHERE b=1 调整索引或查询
范围查询后的列 WHERE a>1 AND b=2 调整索引顺序

记住:写完SQL先EXPLAIN,养成习惯。

相关推荐
止语Lab44 分钟前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活44 分钟前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困1 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn1 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
用户298698530141 小时前
Python 实现 Excel 与 Markdown 互转的实用指南
后端·python·excel
用户77283104908401 小时前
krono-job:零侵入、单二进制交付的分布式任务调度平台(开源)
后端
Conan在掘金1 小时前
鸿蒙报错速查:Function lacks ending return statement,返回路径漏 return 就炸,根因 + 真解法
后端
人间凡尔赛2 小时前
AI-Native 云原生架构:2026 年从容器编排到智能体编排的范式革命
后端·云原生·架构
IT_陈寒2 小时前
Vue的响应式让我加班到凌晨3点,原来问题出在这
前端·人工智能·后端
卷无止境2 小时前
提升 Python 代码健壮性的方法大盘点
后端·python