找回密码
 立即注册
首页 业界区 业界 明明是同一条SQL,为什么有时候走索引a,有时候却走索引 ...

明明是同一条SQL,为什么有时候走索引a,有时候却走索引b ?

崔和美 2025-6-3 00:30:45
前言

想象你是一家餐厅的服务员,面前有两个菜单:

  • 菜单A:按菜品分类排列(前菜、主菜、甜点)
  • 菜单B:按价格从低到高排列
当顾客说:"我要最便宜的川菜"。
你会:

  • 先用菜单B找到所有低价菜
  • 从中筛选川菜
或者:

  • 先用菜单A找到所有川菜
  • 再按价格排序
这就是MySQL优化器的日常决策
明明是同一条SQL,有时候走的索引a,而有时候走的索引b,就是它的锅。
今天这篇文章跟大家一起聊聊,MySQL选错索引的问题,希望对你会有所帮助。
1 一个让程序员崩溃的案例

现在有个需求:查询今年开始已付款的前100个订单。
给status字段创建了索引idx_status。
给create_time字段创建了索引idx_create_time。
查询订单的sql如下:
  1. SELECT * FROM orders
  2. WHERE status = 'paid'      -- 状态条件
  3. AND create_time > '2025-01-01' -- 时间条件
  4. ORDER BY amount DESC
  5. LIMIT 100;
复制代码
周一执行计划如下
  1. 使用索引:idx_status(状态索引)  
  2. 扫描行数:500行  
  3. 耗时:0.1秒
复制代码
周二执行计划如下
  1. 使用索引:idx_create_time(时间索引)  
  2. 扫描行数:50万行  
  3. 耗时:8秒
复制代码
周一只扫描了500行数据,而周二却扫描了50万行数据。
周一耗时0.1秒,而周二耗时却又8秒。
同一SQL在不同时间性能差异80倍!
让我们拆解背后的原因。
2 揭秘优化器的"决策三步曲"

MySQL优化器的决策流程如下:
1.webp

成本计算示例
索引名称预估扫描行数回表次数排序成本总成本idx_status50万50万次需要排序1050分idx_create_time5万5万次无需排序600分根据扫描行数、回表次数、排序成本,计算一个总成本的分数。
优化器会选择总成本更低的idx_create_time索引。
3 导致索引切换的四大真凶

真凶1:数据分布变化

场景还原

  • 周一数据:已支付订单5万条,其中2025年的5万条
  • 周二数据:已支付订单50万条,其中2025年的50万条
这个例子中数据分布变化很大,周二的数据,比周一的数据一下子多了45万。
可能会影响总成本的分数。
我们可以通过下面的SQL查看数据分布:
  1. SELECT
  2.   COUNT(*) AS total,
  3.   SUM(status='paid') AS paid_count,
  4.   SUM(create_time>'2023-01-01') AS new_orders
  5. FROM orders;
复制代码
真凶2:统计信息过期

统计信息过期,就像用去年的地图导航,新修的路不会出现在地图上。
MySQL的"地图"就是统计信息。
我们可以通过ANALYZE TABLE ... DELETE STATISTICS命令删除统计信息:
  1. ANALYZE TABLE orders DELETE STATISTICS;
复制代码
这时候查询可能变成全表扫描:
  1. EXPLAIN SELECT...
复制代码
显示type: ALL
那么,如何解决这个问题呢?
使用ANALYZE TABLE命令,刷新统计信息(相当于更新地图):
  1. ANALYZE TABLE orders;
复制代码
真凶3:索引覆盖度差异

点餐类比

  • 菜单A能直接看到菜品价格 → 无需问厨师(覆盖索引)
  • 菜单B只能看到菜品名 → 需要问厨师详情(回表查询)
下面的SQL会走idx_status(需要回表):
  1. SELECT * FROM orders WHERE status='paid';
复制代码
下面的SQL会走idx_create_time(覆盖索引):
  1. SELECT create_time FROM
  2. orders WHERE create_time>'2023-01-01';
复制代码
真凶4:索引碎片化

索引碎片化就像书本的目录页被撕破,找内容变得困难。
检查方法
  1. SHOW TABLE STATUS LIKE 'orders';
复制代码
查看Data_free字段,值越大碎片越多。
优化方案
使用ALTER TABLE命令重建索引。
  1. ALTER TABLE orders ENGINE=INNODB;
复制代码
4 问题排查四步法

第一步:查看当前执行计划

使用EXPLAIN查看当前SQL的执行计划:
  1. EXPLAIN
  2. SELECT * FROM orders
  3. WHERE status='paid'
  4. AND create_time>'2023-01-01';
复制代码
第二步:检查统计信息

使用SHOW INDEX命令检查索引的统计信息:
  1. SHOW INDEX FROM orders;
复制代码
关注Cardinality字段,值越接近真实数据越好。
第三步:分析数据分布

使用下面的SQL分析数据分布:
  1. SELECT
  2.   COUNT(*) AS total,
  3.   AVG(LENGTH(status)) AS status_avg_len
  4. FROM orders;
复制代码
第四步:追踪优化器思考过程
  1. SET optimizer_trace="enabled=on";
  2. SELECT * FROM orders WHERE ...;
  3. SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE;
复制代码
开启optimizer_trace,然后通过INFORMATION_SCHEMA.OPTIMIZER_TRACE表查看追踪优化器思考过程。
5 三大终极解决方案

方案1:引导优化器选择

使用FORCE INDEX强制使用指定索引:
  1. SELECT * FROM orders FORCE INDEX(idx_status) WHERE ...;
复制代码
方案2:创建更优索引

创建更优的联合索引:
  1. ALTER TABLE orders
  2. ADD INDEX idx_status_create_time(status,create_time);
复制代码
方案3:定期维护计划


  • 定期统计信息更新
  • 定期碎片率检查
  • 定期索引重建
总结

六个必须检查的点


  • WHERE条件字段是否有合适索引
  • ORDER BY/GROUP BY是否利用索引排序
  • 统计信息是否最新(尤其大表每天更新)
  • 是否存在索引碎片(每月检查一次)
  • 是否出现索引合并(INDEX_MERGE)
  • 是否使用覆盖索引(减少回表)
三条黄金法则


  • 二八定律:20%的索引满足80%的查询
  • 数据驱动:定期分析查询模式调整索引
  • 防御编程:核心查询明确指定索引
最后说一句(求关注,别白嫖我)
如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,我的所有文章都会在公众号上首发,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。
本文收录于我的技术网站:http://www.susan.net.cn

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册