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(覆盖索引),再关联取全量数据。

相关推荐
q_19132846952 小时前
基于Springboot+Vue.js的工业人身安全监测系统
vue.js·spring boot·后端·mysql·计算机毕业设计·串口通讯
子夜江寒3 小时前
Python 操作 MySQL 数据库
数据库·python·mysql
野蛮人6号3 小时前
p29 docker08-docker基础-本地目录挂载 无法正确添加mysql 点击更新后data没有正常显示
mysql·docker·容器
Alex Gram3 小时前
SQL Server实时同步到MySQL:构建高效跨数据库数据流通方案
数据库·mysql·sqlserver
Lisonseekpan3 小时前
UUID vs 自增ID做主键,哪个好?
java·数据库·后端·mysql
cnxy1884 小时前
MySQL排序规则深度解析:utf8mb4_0900_ai_ci vs utf8mb4_general_ci完整对比指南
mysql·ci/cd
郑州光合科技余经理4 小时前
技术解析:如何打造适应多国市场的海外跑腿平台
java·开发语言·javascript·mysql·spring cloud·uni-app·php
瀚高PG实验室5 小时前
在Highgo DB 中创建MySQL兼容函数datediff
数据库·mysql·瀚高数据库
czlczl200209255 小时前
SpringBoot实践:从验证码到业务接口的完整交互生命周期
java·spring boot·redis·后端·mysql·spring