想象你是一家餐厅的服务员,面前有两个菜单:
当顾客说:"我要最便宜的川菜"。
你会:
或者:
这就是MySQL优化器的日常决策!
明明是同一条SQL,有时候走的索引a,而有时候走的索引b,就是它的锅。
今天这篇文章跟大家一起聊聊,MySQL选错索引的问题,希望对你会有所帮助。
现在有个需求:查询今年开始已付款的前100个订单。
给status字段创建了索引idx_status。
给create_time字段创建了索引idx_create_time。
查询订单的sql如下:
SELECT * FROM orders
WHERE status = 'paid' -- 状态条件
AND create_time > '2025-01-01' -- 时间条件
ORDER BY amount DESC
LIMIT 100;
周一执行计划如下:
使用索引:idx_status(状态索引)
扫描行数:500行
耗时:0.1秒
周二执行计划如下:
使用索引:idx_create_time(时间索引)
扫描行数:50万行
耗时:8秒
周一只扫描了500行数据,而周二却扫描了50万行数据。
周一耗时0.1秒,而周二耗时却又8秒。
同一SQL在不同时间性能差异80倍!
让我们拆解背后的原因。
MySQL优化器的决策流程如下:
成本计算示例:
索引名称 | 预估扫描行数 | 回表次数 | 排序成本 | 总成本 |
---|---|---|---|---|
idx_status | 50万 | 50万次 | 需要排序 | 1050分 |
idx_create_time | 5万 | 5万次 | 无需排序 | 600分 |
根据扫描行数、回表次数、排序成本,计算一个总成本的分数。
优化器会选择总成本更低的idx_create_time索引。
场景还原:
这个例子中数据分布变化很大,周二的数据,比周一的数据一下子多了45万。
可能会影响总成本的分数。
我们可以通过下面的SQL查看数据分布:
SELECT
COUNT(*) AS total,
SUM(status='paid') AS paid_count,
SUM(create_time>'2023-01-01') AS new_orders
FROM orders;
统计信息过期,就像用去年的地图导航,新修的路不会出现在地图上。
MySQL的"地图"就是统计信息。
我们可以通过ANALYZE TABLE ... DELETE STATISTICS命令删除统计信息:
ANALYZE TABLE orders DELETE STATISTICS;
这时候查询可能变成全表扫描:
EXPLAIN SELECT...
显示type: ALL
那么,如何解决这个问题呢?
使用ANALYZE TABLE命令,刷新统计信息(相当于更新地图):
ANALYZE TABLE orders;
点餐类比:
下面的SQL会走idx_status(需要回表):
SELECT * FROM orders WHERE status='paid';
下面的SQL会走idx_create_time(覆盖索引):
SELECT create_time FROM
orders WHERE create_time>'2023-01-01';
索引碎片化就像书本的目录页被撕破,找内容变得困难。
检查方法:
SHOW TABLE STATUS LIKE 'orders';
查看Data_free字段,值越大碎片越多。
优化方案:
使用ALTER TABLE命令重建索引。
ALTER TABLE orders ENGINE=INNODB;
使用EXPLAIN查看当前SQL的执行计划:
EXPLAIN
SELECT * FROM orders
WHERE status='paid'
AND create_time>'2023-01-01';
使用SHOW INDEX命令检查索引的统计信息:
SHOW INDEX FROM orders;
关注Cardinality字段,值越接近真实数据越好。
使用下面的SQL分析数据分布:
SELECT
COUNT(*) AS total,
AVG(LENGTH(status)) AS status_avg_len
FROM orders;
SET optimizer_trace="enabled=on";
SELECT * FROM orders WHERE ...;
SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE;
开启optimizer_trace,然后通过INFORMATION_SCHEMA.OPTIMIZER_TRACE表查看追踪优化器思考过程。
使用FORCE INDEX强制使用指定索引:
SELECT * FROM orders FORCE INDEX(idx_status) WHERE ...;
创建更优的联合索引:
ALTER TABLE orders
ADD INDEX idx_status_create_time(status,create_time);
最后欢迎加入苏三的星球,你将获得:AI开发项目课程、苏三AI项目、商城微服务实战、秒杀系统实战、商城系统实战、秒杀系统实战、代码生成工具、系统设计、性能优化、技术选型、底层原理、Spring源码解读、工作经验分享、痛点问题、面试八股文等多个优质专栏。