视频看了几百小时还迷糊?关注我,几分钟让你秒懂!
在日常开发中,我们经常会遇到 SQL 查询慢得像蜗牛的情况。明明数据量不大,却查个几秒钟甚至十几秒——这时候,MySQL 索引就是你最该检查的地方!
今天我们就用Java + Spring Boot的方式,结合真实业务场景,手把手带你搞懂 MySQL 索引的使用、误区和最佳实践。
一、什么是索引?为什么需要它?
想象一下你在一本 500 页的字典里找“苹果”这个词:
- 没有索引:你得一页一页翻,直到找到。
- 有索引(目录):直接翻到“P”开头的部分,快速定位。
数据库索引同理:它是对表中一列或多列的值进行排序的一种结构,用来加速查询速度。
二、常见索引类型(MySQL InnoDB 引擎)
| 类型 | 说明 |
|---|---|
| 主键索引(PRIMARY KEY) | 唯一、非空,一张表只能有一个 |
| 唯一索引(UNIQUE) | 值唯一,可为空(但只能有一个 NULL) |
| 普通索引(INDEX / KEY) | 最常用,允许重复、允许 NULL |
| 联合索引(复合索引) | 多个字段组合成一个索引,遵循最左前缀原则 |
| 全文索引(FULLTEXT) | 用于文本搜索(如文章内容),不适用于普通等值/范围查询 |
⚠️ 注意:InnoDB 默认使用 B+ 树实现索引,MyISAM 也是,但存储结构不同。
三、真实业务场景:用户订单查询慢
📌 需求背景
某电商系统,order_info表结构如下:
CREATE TABLE `order_info` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `order_status` TINYINT NOT NULL COMMENT '0-待支付,1-已支付,2-已取消', `create_time` DATETIME NOT NULL, `amount` DECIMAL(10,2) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB;现在有个接口:查询某个用户最近 30 天的所有已支付订单。
// OrderService.java public List<OrderInfo> findPaidOrdersByUser(Long userId, LocalDateTime startTime) { return orderMapper.selectByUserIdAndStatusAndTime(userId, 1, startTime); }对应的 SQL 可能是:
SELECT * FROM order_info WHERE user_id = ? AND order_status = 1 AND create_time >= ?❌ 反例:无索引,全表扫描
如果order_info表有 100 万条数据,而没有任何索引(除了主键),每次查询都要扫描整张表 ——性能灾难!
你可以用EXPLAIN查看执行计划:
EXPLAIN SELECT * FROM order_info WHERE user_id = 123 AND order_status = 1 AND create_time >= '2026-01-01';结果会显示:
type: ALL(全表扫描)rows: 1000000(扫描了百万行)Extra: Using where
这就是典型的“慢查询”根源!
四、正确做法:创建联合索引
✅ 正确索引设计
根据查询条件:user_id、order_status、create_time
我们应该创建一个联合索引:
ALTER TABLE order_info ADD INDEX idx_user_status_time (user_id, order_status, create_time);为什么顺序是
user_id → order_status → create_time?
user_id是高区分度字段(每个用户订单少)order_status是等值查询(=1)create_time是范围查询(>=),放最后(B+树特性决定)
✅ 执行计划验证
再次执行EXPLAIN:
EXPLAIN SELECT * FROM order_info WHERE user_id = 123 AND order_status = 1 AND create_time >= '2026-01-01';结果:
type: rangekey: idx_user_status_timerows: 50(只扫描几十行)Extra: Using index condition
✅ 查询速度从 2 秒降到 10 毫秒!
五、Spring Boot 代码实战
1. 实体类
// OrderInfo.java @Data @TableName("order_info") public class OrderInfo { private Long id; private Long userId; private Integer orderStatus; private LocalDateTime createTime; private BigDecimal amount; }2. Mapper 接口(MyBatis-Plus)
// OrderMapper.java @Mapper public interface OrderMapper extends BaseMapper<OrderInfo> { @Select("SELECT * FROM order_info " + "WHERE user_id = #{userId} " + "AND order_status = #{status} " + "AND create_time >= #{startTime}") List<OrderInfo> selectByUserIdAndStatusAndTime( @Param("userId") Long userId, @Param("status") Integer status, @Param("startTime") LocalDateTime startTime ); }3. Service 调用
@Service public class OrderService { @Autowired private OrderMapper orderMapper; public List<OrderInfo> findRecentPaidOrders(Long userId) { LocalDateTime thirtyDaysAgo = LocalDateTime.now().minusDays(30); return orderMapper.selectByUserIdAndStatusAndTime(userId, 1, thirtyDaysAgo); } }💡 提示:生产环境建议用 MyBatis-Plus 的 LambdaQueryWrapper,更安全。
六、索引使用注意事项(避坑指南)
⚠️ 1. 最左前缀原则
联合索引(a, b, c),以下查询能命中索引:
WHERE a = 1WHERE a = 1 AND b = 2WHERE a = 1 AND b = 2 AND c >= '2026-01-01'
但以下不能或部分命中:
WHERE b = 2❌(跳过 a)WHERE a = 1 AND c = 'xxx'❌(跳过 b,c 无法使用索引)WHERE a > 1 AND b = 2⚠️(a 是范围,b 无法用索引)
⚠️ 2. 不要在索引列上做函数操作
❌ 错误写法:
SELECT * FROM order_info WHERE DATE(create_time) = '2026-01-16';✅ 正确写法:
SELECT * FROM order_info WHERE create_time >= '2026-01-16 00:00:00' AND create_time < '2026-01-17 00:00:00';函数会导致索引失效!
⚠️ 3. 避免冗余索引
比如已有(user_id, order_status),又单独建user_id索引 ——冗余!
因为联合索引的最左前缀已经覆盖了user_id单独查询。
⚠️ 4. 索引不是越多越好
- 插入/更新/删除时要维护索引,写性能下降
- 索引占用磁盘空间
- 建议:单表索引不超过 5 个,联合索引字段不超过 3~4 个
七、如何监控慢查询?
在my.cnf中开启慢查询日志:
slow_query_log = 1 slow_query_log_file = /var/log/mysql/slow.log long_query_time = 1 # 超过1秒记录 log_queries_not_using_indexes = 1 # 记录未使用索引的查询然后定期分析日志,优化 SQL。
八、总结
| 场景 | 是否需要索引 | 建议 |
|---|---|---|
| 高频查询字段 | ✅ 必须 | 如 user_id、status |
| 低区分度字段(如性别) | ❌ 谨慎 | 索引效果差 |
| 经常 ORDER BY / GROUP BY | ✅ 考虑 | 可避免 filesort |
| 大文本字段(如 content) | ❌ 不适合 | 用全文索引或 ES |
记住一句话:索引是把双刃剑,用得好飞天,用不好拖垮数据库!
视频看了几百小时还迷糊?关注我,几分钟让你秒懂!