明明是同一条SQL,为什么有时候走索引a,有时候却走索引b ?
前言
想象你是一家餐厅的服务员,面前有两个菜单:
菜单A:按菜品分类排列(前菜、主菜、甜点)菜单B:按价格从低到高排列当顾客说:"我要最便宜的川菜"。
你会:
先用菜单B找到所有低价菜从中筛选川菜或者:
先用菜单A找到所有川菜再按价格排序这就是MySQL优化器的日常决策!
明明是同一条SQL,有时候走的索引a,而有时候走的索引b,就是它的锅。
今天这篇文章跟大家一起聊聊,MySQL选错索引的问题,希望对你会有所帮助。
1.一个让程序员崩溃的案例
现在有个需求:查询今年开始已付款的前100个订单。
给status字段创建了索引idx_status。
给create_time字段创建了索引idx_create_time。
查询订单的sql如下:
周一执行计划如下:
周二执行计划如下:
周一只扫描了500行数据,而周二却扫描了50万行数据。
周一耗时0.1秒,而周二耗时却又8秒。
同一SQL在不同时间性能差异80倍!
让我们拆解背后的原因。
2.揭秘优化器的"决策三步曲"
MySQL优化器的决策流程如下:
成本计算示例:
索引名称
预估扫描行数
回表次数
排序成本
总成本
idx_status
50万
50万次
需要排序
1050分
idx_create_time
5万
5万次
无需排序
600分
根据扫描行数、回表次数、排序成本,计算一个总成本的分数。
优化器会选择总成本更低的idx_create_time索引。
3.导致索引切换的四大真凶
真凶1:数据分布变化场景还原:
周一数据:已支付订单5万条,其中2025年的5万条周二数据:已支付订单50万条,其中2025年的50万条这个例子中数据分布变化很大,周二的数据,比周一的数据一下子多了45万。
可能会影响总成本的分数。
我们可以通过下面的SQL查看数据分布:
统计信息过期,就像用去年的地图导航,新修的路不会出现在地图上。
MySQL的"地图"就是统计信息。
我们可以通过ANALYZE TABLE ... DELETE STATISTICS命令删除统计信息:
这时候查询可能变成全表扫描:
显示type: ALL
那么,如何解决这个问题呢?
使用ANALYZE TABLE命令,刷新统计信息(相当于更新地图):
点餐类比:
菜单A能直接看到菜品价格 → 无需问厨师(覆盖索引)菜单B只能看到菜品名 → 需要问厨师详情(回表查询)下面的SQL会走idx_status(需要回表):
下面的SQL会走idx_create_time(覆盖索引):
索引碎片化就像书本的目录页被撕破,找内容变得困难。
检查方法:
查看Data_free字段,值越大碎片越多。
优化方案:
使用ALTER TABLE命令重建索引。
4.问题排查四步法
第一步:查看当前执行计划使用EXPLAIN查看当前SQL的执行计划:
使用SHOW INDEX命令检查索引的统计信息:
关注Cardinality字段,值越接近真实数据越好。
第三步:分析数据分布使用下面的SQL分析数据分布:
开启optimizer_trace,然后通过INFORMATION_SCHEMA.OPTIMIZER_TRACE表查看追踪优化器思考过程。
5.三大终极解决方案
方案1:引导优化器选择使用FORCE INDEX强制使用指定索引:
创建更优的联合索引: