MySQL索引设计避坑指南:这些错误别再犯了

同事写了个SQL,生产环境跑了8秒,被DBA追着骂。

一看执行计划,全表扫描,100万行数据一行行扫。

"不是加了索引吗?" "加了,但没用上。"

索引这东西,加得不对比不加还糟糕。整理一下常见的索引坑。

一、索引失效的常见场景

1.1 对索引列做函数运算

sql 复制代码
-- 索引失效
SELECT * FROM orders WHERE YEAR(create_time) = 2024;

-- 索引生效
SELECT * FROM orders 
WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';

对索引列用函数,优化器没法用索引。

同理:

sql 复制代码
-- 失效
SELECT * FROM users WHERE UPPER(username) = 'ADMIN';

-- 如果经常这样查,建函数索引(MySQL 8.0+)
CREATE INDEX idx_username_upper ON users ((UPPER(username)));

1.2 隐式类型转换

sql 复制代码
-- phone是varchar类型
-- 失效:传入数字,会发生隐式转换
SELECT * FROM users WHERE phone = 13812345678;

-- 生效:传入字符串
SELECT * FROM users WHERE phone = '13812345678';

类型不匹配,MySQL会做隐式转换,相当于对列做了函数操作。

1.3 前导模糊查询

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

-- 生效(前缀匹配)
SELECT * FROM products WHERE name LIKE '手机%';

%放前面,没法用B+树的有序性。

解决方案:

  • 用全文索引
  • 用Elasticsearch

1.4 OR条件

sql 复制代码
-- 假设只有name有索引,age没有
-- 失效
SELECT * FROM users WHERE name = '张三' OR age = 25;

-- 解决方案1:给age也加索引
-- 解决方案2:改成UNION
SELECT * FROM users WHERE name = '张三'
UNION
SELECT * FROM users WHERE age = 25;

OR条件会导致索引失效,除非OR两边的列都有索引。

1.5 不等于条件

sql 复制代码
-- 可能失效(取决于数据分布)
SELECT * FROM orders WHERE status != 'completed';

-- 如果status只有几个值,考虑改成
SELECT * FROM orders WHERE status IN ('pending', 'processing', 'failed');

!=NOT IN通常无法利用索引,或者即使用了也是全索引扫描。

二、联合索引的坑

2.1 最左前缀原则

假设有索引:idx_abc (a, b, c)

sql 复制代码
-- 生效
SELECT * FROM t WHERE a = 1;
SELECT * FROM t WHERE a = 1 AND b = 2;
SELECT * FROM t WHERE a = 1 AND b = 2 AND c = 3;
SELECT * FROM t WHERE a = 1 AND c = 3;  -- 只用到a

-- 失效
SELECT * FROM t WHERE b = 2;
SELECT * FROM t WHERE c = 3;
SELECT * FROM t WHERE b = 2 AND c = 3;

联合索引必须从最左列开始使用,中间不能跳过。

2.2 范围查询后的列失效

sql 复制代码
-- 索引:idx_abc (a, b, c)

-- c用不到索引
SELECT * FROM t WHERE a = 1 AND b > 10 AND c = 3;

-- 都能用到
SELECT * FROM t WHERE a = 1 AND b = 10 AND c > 3;

范围查询(>, <, BETWEEN, LIKE)会终止后续列的索引使用。

设计索引时,把等值查询的列放前面,范围查询的列放后面。

2.3 索引列顺序

sql 复制代码
-- 查询1:高频
SELECT * FROM orders WHERE user_id = 1 AND status = 'pending';

-- 查询2:低频  
SELECT * FROM orders WHERE status = 'pending';

-- 正确设计:user_id放前面
CREATE INDEX idx_user_status ON orders (user_id, status);

-- 如果反过来,查询1能用,但查询1效率差(要扫描很多user_id)

高频查询的条件列放前面,区分度高的列放前面。

三、覆盖索引

3.1 什么是覆盖索引

sql 复制代码
-- 索引:idx_user_id_name (user_id, name)

-- 覆盖索引:查询的列都在索引里,不用回表
SELECT user_id, name FROM users WHERE user_id = 1;

-- 非覆盖:需要回表取phone
SELECT user_id, name, phone FROM users WHERE user_id = 1;

覆盖索引避免回表,性能更好。

3.2 利用覆盖索引优化COUNT

sql 复制代码
-- 慢:需要扫描主键索引
SELECT COUNT(*) FROM users;

-- 快:选择最小的二级索引
SELECT COUNT(*) FROM users FORCE INDEX(idx_status);

MySQL会自动选择最小的索引来COUNT,但有时选错了需要手动指定。

四、索引设计原则

4.1 选择性高的列优先

选择性 = 不重复的值 / 总行数

sql 复制代码
-- 查看列的选择性
SELECT 
    COUNT(DISTINCT status) / COUNT(*) AS status_selectivity,
    COUNT(DISTINCT user_id) / COUNT(*) AS user_id_selectivity
FROM orders;

-- 假设结果
-- status_selectivity: 0.0001(5个状态/10万行)
-- user_id_selectivity: 0.8(8万用户/10万行)

user_id选择性高,更适合建索引。

status选择性低,单独建索引意义不大。

4.2 短索引优先

sql 复制代码
-- 对于很长的字符串,可以只索引前缀
CREATE INDEX idx_title ON articles (title(20));

-- 确定前缀长度:保证足够的选择性
SELECT 
    COUNT(DISTINCT LEFT(title, 10)) / COUNT(*) AS sel_10,
    COUNT(DISTINCT LEFT(title, 20)) / COUNT(*) AS sel_20,
    COUNT(DISTINCT title) / COUNT(*) AS sel_full
FROM articles;

前缀索引更短,同样空间能存更多数据,效率更高。

4.3 避免冗余索引

sql 复制代码
-- 冗余:idx_a已经被idx_ab覆盖
CREATE INDEX idx_a ON t (a);
CREATE INDEX idx_ab ON t (a, b);

-- 不冗余:idx_ba的顺序不同
CREATE INDEX idx_ab ON t (a, b);
CREATE INDEX idx_ba ON t (b, a);

定期检查冗余索引:

sql 复制代码
-- MySQL 8.0+
SELECT * FROM sys.schema_redundant_indexes;

4.4 避免过度索引

索引不是越多越好:

  • 占用磁盘空间
  • 插入/更新/删除都要维护索引
  • 优化器选择困难

一般一个表不超过5-6个索引。

五、EXPLAIN看执行计划

5.1 关键字段

sql 复制代码
EXPLAIN SELECT * FROM orders WHERE user_id = 1;
字段 含义 关注点
type 访问类型 const > eq_ref > ref > range > index > ALL
key 实际使用的索引 是否用到预期索引
rows 预估扫描行数 越小越好
Extra 额外信息 Using index好,Using filesort/temporary不好

5.2 常见type解释

sql 复制代码
-- ALL:全表扫描,最差
EXPLAIN SELECT * FROM users WHERE age = 25;  -- age没索引

-- index:全索引扫描
EXPLAIN SELECT id FROM users;

-- range:范围扫描
EXPLAIN SELECT * FROM users WHERE id > 100;

-- ref:非唯一索引等值查询
EXPLAIN SELECT * FROM orders WHERE user_id = 1;

-- eq_ref:唯一索引等值查询
EXPLAIN SELECT * FROM users WHERE id = 1;

-- const:主键/唯一索引等值,最多一行
EXPLAIN SELECT * FROM users WHERE id = 1;

5.3 Extra信息

sql 复制代码
-- Using index:覆盖索引,好
-- Using where:用了WHERE过滤,正常
-- Using temporary:用了临时表,需要优化
-- Using filesort:用了文件排序,需要优化

看到Using temporary或Using filesort,基本都要优化。

六、真实案例

案例1:订单查询优化

原SQL(执行8秒):

sql 复制代码
SELECT * FROM orders 
WHERE user_id = 12345 
  AND status = 'pending'
  AND create_time > '2024-01-01'
ORDER BY create_time DESC
LIMIT 20;

EXPLAIN显示:

  • type: ALL
  • rows: 1000000
  • Extra: Using where; Using filesort

问题:没用到索引,全表扫描+文件排序。

优化:

sql 复制代码
-- 建立联合索引
CREATE INDEX idx_user_status_time ON orders (user_id, status, create_time);

优化后EXPLAIN:

  • type: range
  • rows: 234
  • Extra: Using index condition

执行时间:8ms

案例2:分页查询优化

原SQL:

sql 复制代码
SELECT * FROM logs ORDER BY id DESC LIMIT 100000, 20;

问题:深分页,要扫描10万行再丢弃。

优化方案1:记录上次ID

sql 复制代码
-- 前端传上一页最小ID
SELECT * FROM logs WHERE id < 12345678 ORDER BY id DESC LIMIT 20;

优化方案2:延迟关联

sql 复制代码
SELECT l.* FROM logs l
INNER JOIN (
    SELECT id FROM logs ORDER BY id DESC LIMIT 100000, 20
) AS t ON l.id = t.id;

子查询只查ID(覆盖索引),再关联取全量数据。

相关推荐
渡我白衣12 小时前
【MySQL基础】(2):数据库基础概念
数据库·人工智能·深度学习·神经网络·mysql·机器学习·自然语言处理
怣5013 小时前
MySQL WHERE子句完全指南:精准过滤数据的艺术
数据库·mysql
Fleshy数模1 天前
CentOS7 安装配置 MySQL5.7 完整教程(本地虚拟机学习版)
linux·mysql·centos
az44yao1 天前
mysql 创建事件 每天17点执行一个存储过程
mysql
秦老师Q1 天前
php入门教程(超详细,一篇就够了!!!)
开发语言·mysql·php·db
橘子131 天前
MySQL用户管理(十三)
数据库·mysql
Dxy12393102161 天前
MySQL如何加唯一索引
android·数据库·mysql
我真的是大笨蛋1 天前
深度解析InnoDB如何保障Buffer与磁盘数据一致性
java·数据库·sql·mysql·性能优化
怣501 天前
MySQL数据检索入门:从零开始学SELECT查询
数据库·mysql
人道领域1 天前
javaWeb从入门到进阶(SpringBoot事务管理及AOP)
java·数据库·mysql