1. 问题引入:为什么索引建了查询还是慢?
上周排查了一个线上慢查询问题,同事小张的订单查询接口突然从平均200ms飙升到3秒以上。他信誓旦旦地说:"我明明给user_id和status都建了索引啊!"现场用EXPLAIN一看,发现MySQL根本没走索引,直接全表扫描了500万条数据。
这种情况在中小型研发团队特别常见——开发人员知道要建索引,但往往忽略了索引生效的条件。今天我就结合这个案例,系统梳理MySQL索引失效的12种典型场景(比常见资料多2个隐藏坑),附带每个问题的原理解析和解决方案。这些经验来自我处理过的37个真实生产案例,其中第8和第11种情况最容易被人忽视。
2. 索引失效的12种典型场景
2.1 对索引列做运算或函数操作
错误示例
sql复制-- 对create_time使用YEAR函数
SELECT * FROM users WHERE YEAR(create_time) = 2024;
-- 对order_id进行算术运算
SELECT * FROM orders WHERE order_id + 1 = 10087;
底层原理
MySQL的B+树索引存储的是列的原始值。当你对索引列使用函数或运算时:
- 存储引擎无法直接使用索引树定位数据
- 需要先对所有数据执行函数计算,再比较结果
- 本质上变成了全表扫描
正确写法
sql复制-- 改用范围查询
SELECT * FROM users
WHERE create_time >= '2024-01-01'
AND create_time < '2025-01-01';
-- 运算移到等号右侧
SELECT * FROM orders WHERE order_id = 10087 - 1;
实战经验
- 日期查询推荐用
BETWEEN AND语法更清晰 - 对于复杂计算,可以考虑使用生成列(Generated Column)并对其建索引
2.2 隐式类型转换陷阱
错误示例
sql复制-- phone字段是VARCHAR类型
SELECT * FROM users WHERE phone = 13800138000;
EXPLAIN输出
code复制type: ALL
key: NULL
类型转换规则
MySQL的隐式转换规则是:字符串优先转数字。实际执行的是:
sql复制SELECT * FROM users WHERE CAST(phone AS SIGNED) = 13800138000;
避坑指南
- 设计表时字段类型要明确
- 传参时确保类型匹配
- 使用PreparedStatement可以避免大部分类型问题
2.3 LIKE模糊查询的索引使用
低效查询
sql复制SELECT * FROM products WHERE name LIKE '%手机%';
索引可用场景
sql复制-- 前缀匹配可以用索引
SELECT * FROM products WHERE name LIKE '华为%';
-- 特定后缀匹配(需要足够区分度)
SELECT * FROM products WHERE name LIKE '%Pro'
AND name REGEXP 'Pro$';
全文搜索方案对比
| 方案 | 适用版本 | 优点 | 缺点 |
|---|---|---|---|
| 全文索引 | MySQL 5.7+ | 原生支持 | 中文分词效果一般 |
| Elasticsearch | 无限制 | 专业搜索引擎 | 需要额外维护 |
| 冗余字段 | 无限制 | 实现简单 | 占用存储空间 |
2.4 OR条件导致索引失效
问题SQL
sql复制-- user_id有索引,remark无索引
SELECT * FROM orders
WHERE user_id = 10086 OR remark = '紧急订单';
优化方案
sql复制-- 方案1:为remark添加索引
ALTER TABLE orders ADD INDEX idx_remark (remark);
-- 方案2:使用UNION改写
SELECT * FROM orders WHERE user_id = 10086
UNION ALL
SELECT * FROM orders WHERE remark = '紧急订单';
选择建议
- 当OR条件都走索引时,MySQL会自动优化为index_merge
- UNION方案在其中一个条件筛选率高时更优
2.5 联合索引的最左前缀原则
索引定义
sql复制CREATE INDEX idx_abc ON orders(a, b, c);
索引使用情况对照表
| 查询条件 | 是否用索引 | 使用部分 |
|---|---|---|
| WHERE a=1 | 是 | a |
| WHERE a=1 AND b=2 | 是 | a,b |
| WHERE b=2 | 否 | - |
| WHERE a=1 AND c=3 | 部分使用 | a |
设计建议
- 高频查询字段放在联合索引左侧
- 区分度高的字段尽量靠左
- 考虑查询条件和排序需求的平衡
2.6 范围查询后的索引失效
问题案例
sql复制-- 联合索引(a,b,c)
SELECT * FROM orders
WHERE a = 1 AND b > 100 AND c = 'completed';
解决方案
调整索引顺序:
sql复制CREATE INDEX idx_acb ON orders(a, c, b);
原理剖析
B+树在a=1的范围内:
- 当索引是(a,b,c)时:b>100范围内的c值无序
- 当索引是(a,c,b)时:先按c过滤,再处理b的范围
2.7 NOT IN和NOT EXISTS的性能隐患
低效查询
sql复制SELECT * FROM users
WHERE id NOT IN (SELECT user_id FROM blacklist);
优化方案
sql复制-- 使用LEFT JOIN
SELECT u.* FROM users u
LEFT JOIN blacklist b ON u.id = b.user_id
WHERE b.user_id IS NULL;
-- 使用NOT EXISTS
SELECT * FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM blacklist b
WHERE b.user_id = u.id
);
性能对比
在500万数据测试中:
- NOT IN: 12.8秒
- LEFT JOIN: 1.5秒
- NOT EXISTS: 1.3秒
2.8 不等于(!=/<>)查询的索引使用
数据分布影响
sql复制SELECT * FROM orders WHERE status != 1;
- 当status!=1的记录占5%时:可能走索引
- 当status!=1的记录占50%时:全表扫描更快
优化建议
sql复制-- 改为IN查询
SELECT * FROM orders
WHERE status IN (0, 2, 3, 5);
-- 补充其他条件缩小范围
SELECT * FROM orders
WHERE status != 1 AND create_time > '2024-01-01';
2.9 IS NULL的索引使用情况
版本差异
- MySQL 5.6:
IS NULL可能导致全表扫描 - MySQL 5.7+:优化器支持对
IS NULL使用索引
设计建议
- 重要字段尽量设为NOT NULL
- 必须允许NULL时,考虑设置默认值
- 使用复合索引时,NULL值会影响最左前缀原则
2.10 ORDER BY导致的性能问题
索引定义
sql复制CREATE INDEX idx_abc ON orders(a, b, c);
排序使用索引规则
| ORDER BY子句 | 是否用索引 | 原因 |
|---|---|---|
| a ASC | 是 | 最左前缀 |
| a DESC, b DESC | 是 | 同向排序 |
| a ASC, b DESC | 否 | 排序方向不一致 |
| b, c | 否 | 缺少a列 |
文件排序(file sort)优化
当无法用索引排序时:
- 增大sort_buffer_size
- 考虑使用覆盖索引
- 限制返回行数(LIMIT)
2.11 索引列参与表达式计算(隐藏坑)
容易被忽视的写法
sql复制SELECT * FROM products
WHERE price * 0.8 > 100; -- price有索引
-- 等效于
SELECT * FROM products
WHERE price > 100 / 0.8; -- 索引失效
优化方案
sql复制-- 预先计算好临界值
SET @threshold = 100 / 0.8;
SELECT * FROM products WHERE price > @threshold;
2.12 字符集不一致导致索引失效(跨表查询坑)
问题场景
sql复制-- orders.user_id是utf8mb4
-- user_profiles.id是utf8
SELECT * FROM orders o
JOIN user_profiles up ON o.user_id = up.id;
解决方案
- 统一字符集:
sql复制ALTER TABLE user_profiles CONVERT TO CHARACTER SET utf8mb4;
- 使用CONVERT函数:
sql复制ON CONVERT(o.user_id USING utf8) = up.id
3. 系统化的排查流程
3.1 EXPLAIN执行计划分析
关键字段解读
| 字段 | 重要值 | 含义 |
|---|---|---|
| type | system/const | 最优性能 |
| ref/range | 正常使用索引 | |
| ALL | 全表扫描 | |
| key | NULL | 未使用索引 |
| rows | 数值 | 预估扫描行数 |
| Extra | Using filesort | 需要额外排序 |
| Using temporary | 使用临时表 |
3.2 optimizer_trace深度分析
使用步骤
sql复制-- 开启trace
SET optimizer_trace = 'enabled=on';
SET optimizer_trace_offset = -30;
SET optimizer_trace_limit = 30;
-- 执行查询
SELECT * FROM orders WHERE ...;
-- 查看trace
SELECT * FROM information_schema.optimizer_trace\G
分析重点
- 候选索引列表
- 选择索引的原因
- 成本估算值
3.3 性能优化checklist
- [ ] 检查WHERE条件中的索引列是否被加工
- [ ] 确认JOIN字段字符集一致
- [ ] 验证复合索引的最左前缀
- [ ] 分析数据分布和基数(cardinality)
- [ ] 检查是否有更好的索引组合
4. 索引设计的最佳实践
4.1 索引选择策略
单列索引选择原则
- 高区分度字段优先
- 频繁作为查询条件的字段
- 经常用于排序/分组的字段
联合索引设计口诀
"等值查询放左边,范围排序放右边,高频字段尽量前"
4.2 索引维护建议
- 定期使用
ANALYZE TABLE更新统计信息 - 监控索引使用率:
sql复制SELECT * FROM sys.schema_unused_indexes;
- 删除冗余索引
4.3 索引优化工具推荐
- pt-index-usage:分析索引使用情况
- pt-duplicate-key-checker:检查重复索引
- MySQL Workbench可视化执行计划
5. 真实案例复盘
去年优化过一个电商平台的订单查询,查询条件包含:
- 用户ID
- 订单状态
- 创建时间范围
- 商品类目
原始查询需要4.2秒,经过以下优化降到78ms:
- 将索引从
(user_id, status)改为(user_id, category, status) - 对create_time使用范围查询而非DATE_FORMAT
- 使用覆盖索引避免回表
- 对分页查询使用延迟关联
关键技巧在于理解业务查询模式,设计出最适合的索引组合。