MySQL 索引设计实战指南

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 '订单操作日志表';

常见查询场景:

  1. 根据用户ID查询操作记录
  2. 根据订单号查询操作记录
  3. 根据操作状态 + 重试次数 + 创建时间查询待重试记录
  4. 根据操作类型 + 创建时间查询某类操作的历史
  5. 根据用户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';

关注 typekeyrows 字段:

  • 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 优化索引
相关推荐
字节跳动开源2 小时前
Viking AI 搜索 CLI 正式发布:会说话,就能做搜索推荐
数据库·人工智能·开源
TechWJ3 小时前
数据库在公司内网,出差路上想查数据怎么办?
服务器·数据库·mariadb
我是一颗柠檬3 小时前
【MySQL全面教学】MySQL事务与ACID Day9(2026年)
数据库·后端·mysql
橙子圆1233 小时前
Redis知识9之集群
数据库·redis·缓存
BlackHeart12033 小时前
【SQL】Oracle中序列(Sequence)作为默认值引发的ORA-00979
数据库·sql·oracle
rocpp4 小时前
Android 相册选择与拍照接入实践:MediaStore 分页、权限适配与 FileProvider
android
bug菌4 小时前
【SpringBoot 3.x 第254节】夯爆了,数据库访问性能优化实战详解!
数据库·spring boot·后端
xxl大卡4 小时前
MySQL的执行流程
数据库·mysql
chicheese4 小时前
MySQL优化实践:选错JOIN 驱动表,性能相差几十倍
数据库·mysql
無限進步D4 小时前
MySQL 单行函数
数据库·mysql