1. 为什么我们需要关注索引优化?
上周排查一个生产环境慢查询时,我发现一条看似简单的订单统计SQL竟然执行了8秒。通过EXPLAIN分析执行计划后,发现问题出在缺失的复合索引上。加上合适的索引后,查询时间直接降到200毫秒——性能提升了40倍!这个案例让我再次意识到,合理的索引策略对数据库性能有多重要。
索引就像书籍的目录,没有索引的数据库就像一本没有目录的百科全书,每次查找都需要逐页翻阅。但索引也不是越多越好,错误的索引反而会拖慢写入速度并占用额外存储空间。本文将分享我在MySQL索引优化方面的实战经验,涵盖索引类型选择、复合索引设计、执行计划解读等核心内容,并附上多个真实案例的优化前后对比。
2. 索引基础与核心原理
2.1 索引的底层实现剖析
MySQL最常用的InnoDB引擎采用B+树作为索引结构。与普通二叉树不同,B+树具有以下特点:
- 每个节点可以包含多个键值(默认16KB页大小)
- 所有数据都存储在叶子节点,形成有序链表
- 非叶子节点只存储键值和子节点指针
这种结构使得范围查询效率极高。例如查询WHERE id BETWEEN 100 AND 200时,只需定位到100所在的叶子节点,然后沿着链表扫描即可。
2.2 索引类型选型指南
主键索引(PRIMARY KEY):
- 必须唯一且非空
- InnoDB的表数据本身就是主键索引组织表(IOT)
- 自增ID vs UUID:自增ID写入性能更好,避免页分裂
普通索引(INDEX):
- 最基本的索引类型
- 适合高选择性列(如手机号、邮箱)
- 存储列值+主键指针
唯一索引(UNIQUE):
- 保证列值唯一性
- NULL值在唯一索引中视为特殊值(可存在多个NULL)
全文索引(FULLTEXT):
- 专为文本搜索设计
- 支持MATCH AGAINST语法
- 仅适用于MyISAM和InnoDB(5.6+)
3. 复合索引设计实战
3.1 最左前缀原则深度解析
复合索引(A,B,C)实际相当于建立了:
- (A)
- (A,B)
- (A,B,C)
三个索引。以下SQL能否使用该索引?
sql复制SELECT * FROM table WHERE B=1 AND A=2; -- 可以,优化器会调整顺序
SELECT * FROM table WHERE A=2 ORDER BY C; -- 只能用到A列索引
SELECT * FROM table WHERE B=1; -- 无法使用
3.2 索引列顺序决策矩阵
推荐按以下优先级排序:
- 等值条件列(WHERE a=1)
- 范围条件列(WHERE b>10)
- 排序字段(ORDER BY c)
- 分组字段(GROUP BY d)
- 覆盖索引需要的列
案例:用户订单查询场景
sql复制SELECT user_id, order_date, amount
FROM orders
WHERE user_id=123
AND order_date BETWEEN '2023-01-01' AND '2023-03-31'
ORDER BY amount DESC;
最优索引应为:(user_id, order_date, amount)
4. 执行计划深度解读
4.1 EXPLAIN关键指标详解
sql复制EXPLAIN SELECT * FROM products WHERE category='electronics' AND price>1000;
重点关注:
- type:从优到差 system > const > eq_ref > ref > range > index > ALL
- key:实际使用的索引
- rows:预估扫描行数
- Extra:
Using index:覆盖索引Using filesort:需要额外排序Using temporary:使用临时表
4.2 索引失效的七大陷阱
- 对索引列使用函数:
WHERE DATE(create_time)='2023-01-01' - 隐式类型转换:
WHERE user_id='100'(user_id是int) - 前导模糊查询:
WHERE name LIKE '%张' - OR条件未全覆盖:
WHERE a=1 OR b=2(需分别为a、b建索引) - 不符合最左前缀
- 使用!=或<>操作符
- 列对比:
WHERE a=b
5. 高级优化策略
5.1 覆盖索引优化术
当查询的所有列都包含在索引中时,无需回表:
sql复制-- 原始SQL(需要回表)
SELECT * FROM users WHERE age>20;
-- 优化为覆盖索引
ALTER TABLE users ADD INDEX idx_age_name(age, name);
SELECT age, name FROM users WHERE age>20;
5.2 索引下推(ICP)
MySQL 5.6+特性,将WHERE条件过滤下推到存储引擎层:
sql复制-- 没有ICP时:
1. 通过索引定位到满足age>20的记录
2. 回表读取完整数据
3. 在Server层过滤gender='M'
-- 启用ICP后:
1. 在索引层同时过滤age>20 AND gender='M'
2. 只对符合条件的记录回表
通过以下参数控制:
sql复制SET optimizer_switch='index_condition_pushdown=on';
6. 实战优化案例集锦
6.1 电商订单查询优化
问题SQL:
sql复制SELECT * FROM orders
WHERE user_id=123
AND status='completed'
AND create_time>'2023-01-01'
ORDER BY total_amount DESC
LIMIT 10;
优化步骤:
- 分析WHERE条件选择性:user_id=123(高) > status='completed'(中) > create_time(范围)
- 确认排序字段:total_amount
- 创建复合索引:
(user_id, status, create_time, total_amount) - 改写为覆盖索引查询:
sql复制SELECT order_id, user_id, status, create_time, total_amount
FROM orders
WHERE user_id=123
AND status='completed'
AND create_time>'2023-01-01'
ORDER BY total_amount DESC
LIMIT 10;
效果:执行时间从2.3s降至80ms
6.2 社交平台Feed流优化
问题场景:
分页查询好友动态出现深度分页性能问题:
sql复制SELECT * FROM feeds
WHERE user_id IN (SELECT friend_id FROM relations WHERE user_id=123)
ORDER BY create_time DESC
LIMIT 10000, 20;
优化方案:
- 使用JOIN替代子查询
- 采用"记住上次位置"分页法:
sql复制SELECT * FROM feeds f
JOIN relations r ON f.user_id=r.friend_id
WHERE r.user_id=123
AND f.create_time < '2023-06-01 12:00:00' -- 上次查询的最后一条时间
ORDER BY f.create_time DESC
LIMIT 20;
索引设计:
- feeds表:
(user_id, create_time) - relations表:
(user_id, friend_id)
7. 索引监控与维护
7.1 索引使用情况分析
查看未使用的索引:
sql复制SELECT * FROM sys.schema_unused_indexes;
索引统计信息:
sql复制SHOW INDEX FROM table_name;
-- Cardinality/估算基数越接近表行数,选择性越高
7.2 索引碎片整理
当索引碎片率>30%时应重建:
sql复制-- 查看碎片率
SELECT table_name, index_name,
ROUND(data_free/(data_length+index_length)*100,2) AS frag_ratio
FROM information_schema.tables
WHERE engine='InnoDB';
-- 重建索引
ALTER TABLE orders ENGINE=InnoDB;
-- 或
OPTIMIZE TABLE orders;
8. 常见误区与避坑指南
-
过度索引:每新增一个索引,写操作需要额外维护索引树。建议单表索引不超过5个
-
盲目使用外键:外键虽然保证数据一致性,但会导致锁竞争。高并发系统建议在应用层实现约束
-
UUID作为主键:随机UUID导致频繁页分裂。如果必须使用,考虑有序UUID(UUIDv7)或自增ID+业务ID组合
-
忽视隐式排序:即使不需要排序,也应考虑常用排序条件,避免临时表排序
-
统计信息过期:ANALYZE TABLE定期更新统计信息,特别是大表数据变化超过10%时