MySQL 索引设计实战指南
一、基础概念
1.1 什么是索引
索引是数据库中用于加速数据检索的数据结构。MySQL 中最常用的索引类型是 B+Tree 索引,它将数据按照索引列的值有序组织,使得查询可以通过二分查找快速定位数据,而不需要全表扫描。
1.2 索引的代价
- 写入开销:每次 INSERT/UPDATE/DELETE 都需要维护索引结构
- 存储空间:每个索引都是一棵独立的 B+Tree,占用磁盘空间
- 维护成本:索引越多,优化器选择执行计划的复杂度越高
因此,索引不是越多越好,需要根据实际查询场景合理设计。
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
二、示例场景
假设我们有一个电商平台的订单操作日志表:
sql
CREATE TABLE order_operation_log (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL COMMENT '用户ID',
order_no VARCHAR(50) NOT NULL COMMENT '订单号',
op_type TINYINT NOT NULL COMMENT '操作类型:1-创建,2-支付,3-发货,4-签收,5-退款',
op_status TINYINT NOT NULL COMMENT '操作状态:0-成功,1-失败',
op_time DATETIME NOT NULL COMMENT '操作时间',
op_detail TEXT NULL COMMENT '操作详情',
retry_count INT DEFAULT 0 NOT NULL COMMENT '重试次数',
create_time DATETIME NOT NULL COMMENT '创建时间'
) COMMENT '订单操作日志表';
常见查询场景:
- 根据用户ID查询操作记录
- 根据订单号查询操作记录
- 根据操作状态 + 重试次数 + 创建时间查询待重试记录
- 根据操作类型 + 创建时间查询某类操作的历史
- 根据用户ID + 订单号联合查询
三、单列索引 vs 复合索引
3.1 单列索引
sql
CREATE INDEX idx_user_id ON order_operation_log (user_id);
CREATE INDEX idx_order_no ON order_operation_log (order_no);
CREATE INDEX idx_op_status ON order_operation_log (op_status);
单列索引适用于只按单个字段查询的场景。
3.2 复合索引(联合索引)
sql
CREATE INDEX idx_status_retry_time ON order_operation_log (op_status, retry_count, create_time);
CREATE INDEX idx_type_time ON order_operation_log (op_type, create_time);
CREATE INDEX idx_user_order ON order_operation_log (user_id, order_no);
复合索引将多个列组合成一个索引,适用于多条件联合查询。
四、最左前缀原则
4.1 核心规则
复合索引遵循最左前缀原则:查询条件必须从索引的最左列开始,按顺序使用,才能命中索引。
以 idx_status_retry_time (op_status, retry_count, create_time) 为例:
| 查询条件 | 是否命中索引 | 说明 |
|---|---|---|
WHERE op_status = 0 |
✅ 命中 | 使用了最左列 |
WHERE op_status = 0 AND retry_count < 3 |
✅ 命中 | 使用了前两列 |
WHERE op_status = 0 AND retry_count < 3 AND create_time > '2024-01-01' |
✅ 命中 | 使用了全部三列 |
WHERE retry_count < 3 |
❌ 不命中 | 跳过了最左列 |
WHERE create_time > '2024-01-01' |
❌ 不命中 | 跳过了前两列 |
WHERE op_status = 0 AND create_time > '2024-01-01' |
⚠️ 部分命中 | 只用到 op_status,跳过了 retry_count |
4.2 关键推论
复合索引 (A, B, C) 等价于同时拥有以下索引能力:
(A)--- 单独查 A(A, B)--- 查 A + B(A, B, C)--- 查 A + B + C
但不等价于:
(B)或(C)或(B, C)
五、索引冗余判定
5.1 什么是冗余索引
如果一个索引是另一个复合索引的最左前缀,那么这个索引就是冗余的。
5.2 冗余示例
sql
-- 冗余!idx_user_id 是 idx_user_order 的最左前缀
CREATE INDEX idx_user_id ON order_operation_log (user_id);
CREATE INDEX idx_user_order ON order_operation_log (user_id, order_no);
-- 冗余!idx_op_status 是 idx_status_retry_time 的最左前缀
CREATE INDEX idx_op_status ON order_operation_log (op_status);
CREATE INDEX idx_status_retry_time ON order_operation_log (op_status, retry_count, create_time);
判定方法:如果索引 A 的所有列是索引 B 的前 N 列(顺序一致),则 A 冗余。
5.3 非冗余示例
sql
-- 不冗余!虽然都包含 user_id,但 idx_user_time 的第二列不同
CREATE INDEX idx_user_order ON order_operation_log (user_id, order_no);
CREATE INDEX idx_user_time ON order_operation_log (user_id, create_time);
这两个索引服务于不同的查询场景,不构成冗余。
5.4 冗余索引的危害
- 浪费磁盘空间
- 增加写入时的索引维护开销
- 增加优化器选择索引的复杂度
- 可能导致优化器选错索引
六、索引设计原则
6.1 高选择性列优先
选择性 = 不同值的数量 / 总行数。选择性越高,索引过滤效果越好。
sql
-- 好:order_no 选择性极高(几乎唯一)
CREATE INDEX idx_order_no ON order_operation_log (order_no);
-- 差:op_status 只有 0/1 两个值,选择性极低
CREATE INDEX idx_op_status ON order_operation_log (op_status);
低选择性列单独建索引意义不大,但作为复合索引的一部分可以有效缩小范围。
6.2 复合索引列顺序
推荐顺序:等值查询列 → 范围查询列
sql
-- 好:op_status 等值查询在前,create_time 范围查询在后
CREATE INDEX idx_status_time ON order_operation_log (op_status, create_time);
-- 差:范围查询列在前,后面的列无法使用索引
CREATE INDEX idx_time_status ON order_operation_log (create_time, op_status);
原因:B+Tree 中,范围查询之后的列无法继续利用索引有序性。
6.3 覆盖索引
如果查询的所有列都包含在索引中,MySQL 可以直接从索引返回数据,无需回表。
sql
-- 如果经常执行:SELECT order_no, op_time FROM order_operation_log WHERE user_id = ?
CREATE INDEX idx_user_order_time ON order_operation_log (user_id, order_no, op_time);
此时查询可以完全通过索引完成,性能最优。
6.4 避免过度索引
经验法则:
- 单表索引数量建议不超过 5-6 个
- 写多读少的表(如日志表)更要控制索引数量
- 定期审查慢查询日志,按需添加索引
七、索引失效的常见场景
| 场景 | 示例 | 原因 |
|---|---|---|
| 对索引列使用函数 | WHERE DATE(create_time) = '2024-01-01' |
函数破坏了索引有序性 |
| 隐式类型转换 | WHERE order_no = 12345(order_no 是 VARCHAR) |
字符串列与数字比较触发转换 |
| LIKE 左模糊 | WHERE order_no LIKE '%ABC' |
无法利用 B+Tree 的有序性 |
| OR 条件 | WHERE user_id = 1 OR op_type = 3 |
除非两列都有索引且优化器选择 index_merge |
| 不满足最左前缀 | 复合索引 (A,B,C),查询只用 B | 跳过了最左列 |
| IS NULL / IS NOT NULL | 视情况而定 | 某些版本/场景下可能不走索引 |
八、实用工具
8.1 查看表的索引
sql
SHOW INDEX FROM order_operation_log;
8.2 分析查询是否使用索引
sql
EXPLAIN SELECT * FROM order_operation_log WHERE user_id = 100 AND order_no = 'ORD001';
关注 type、key、rows 字段:
type = ref/range表示使用了索引type = ALL表示全表扫描key显示实际使用的索引名
8.3 查找冗余索引
sql
-- 使用 sys 库(MySQL 5.7+)
SELECT * FROM sys.schema_redundant_indexes WHERE table_schema = 'your_database';
九、总结
| 要点 | 说明 |
|---|---|
| 最左前缀原则 | 复合索引从左到右匹配,跳列则后续列失效 |
| 冗余判定 | 单列索引是某复合索引的最左前缀 → 冗余 |
| 列顺序 | 等值列在前,范围列在后 |
| 选择性 | 高选择性列更适合建索引 |
| 覆盖索引 | 查询列全在索引中 → 无需回表 |
| 写入代价 | 索引越多,INSERT/UPDATE 越慢 |
| 定期审查 | 结合慢查询日志和 EXPLAIN 优化索引 |