1. 优化器在“算计”什么:先说清楚EXPLAIN里那一行ALL的含义
我先给结论:MySQL的优化器并不会因为你建了索引就感动到必须用。它只关心一件事——这套执行方案的综合成本是否够低。如果它计算出全表扫描比走索引还便宜,那它就会心安理得地选择全表扫描,哪怕你刚给这张表建了三个二级索引。
很多开发同学第一次遇到这种情况都非常崩溃。明明一条SQL从语法上完全可以用到索引,条件列也建了索引,EXPLAIN结果里却写着type: ALL,key: NULL,rows还显示几十万。我见过不少同事第一反应就是“是不是索引没建成功”,然后一顿SHOW INDEX查证,结果索引好好地在那躺着。问题根本不出在索引是否存在,而是优化器在成本计算时把“走索引”的方案直接否掉了。
要弄清这件事,得先理解MySQL优化器的运行逻辑。它不是一个见索引就冲的愣头青,而是一个精打细算的“成本会计”。在生成最终执行计划之前,优化器会枚举出它认为有可能执行的路径,然后分别为每条路径估算成本,比如“全表扫描大约要花多少I/O和CPU”“走某个二级索引回表要花多少随机I/O”,最后挑成本最低的那条当最终方案。这个估算过程依赖的并不是实时数据,而是一堆从统计信息里拿到的“数字快照”。
日常开发中,判断一条SQL是否会走索引,最直接的手段就是看EXPLAIN输出。你需要重点读四个信息:type、key、rows和Extra。type为ALL代表全表扫描,key为空代表没有使用任何索引,rows是优化器预估需要扫描的行数,Extra里如果出现Using where说明存储引擎返回数据后Server层又做了条件过滤。很多人只看key,key为空就觉得“索引失效了”,其实没那么简单,真正的决策逻辑藏在优化器的成本模型里。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么明摆着有索引,优化器还是选择全表扫描
2.1 回表成本太高,优化器算完觉得亏了
大家最常忽略的一个因素就是回表(table lookup)。InnoDB里二级索引的叶子节点只存了“索引列的值 + 主键值”,它不像主键索引那样直接包含着完整行数据。你想通过二级索引查出所有列,就必须先拿主键去聚簇索引里再查一遍完整行,这个过程就是回表。
回表是随机I/O,成本很高,而且行数越多,回表次数就越多,成本会线性上升。当一条查询条件能筛选出来的数据行数占全表比例很大时,比如超过20%甚至更多,优化器会倾向于认为:与其通过二级索引筛选、然后回表翻一堆数据页,不如干脆顺序把全表数据从头到尾扫一遍,反正代价差不多,甚至顺序扫描更便宜。
举个例子,一张订单表有100万行,你执行SELECT * FROM orders WHERE status = 0,如果status字段上建了索引,但status=0的数据占了80万行,优化器绝对不会走这个索引。因为它算出来,走索引要扫描80万个主键再回表80万次,而直接全表扫描只需要顺序读一遍100万行,顺序I/O的效率远高于随机I/O,显然后者划算。这也是为什么很多人觉得“给状态字段建索引应该就能加速”,结果被现实狠狠教育了一顿。
2.2 数据量小到扫表比查索引更快
另一个反直觉的情况是:表里只有几百行数据,你建了索引,优化器照样选ALL。原因很简单,InnoDB的最小存储单位是数据页,默认16KB。几百行数据甚至可能只占一个数据页,全表扫描只要顺序读一个页就够了。而走索引的话,要先读索引页,再回到数据页,这中间多了一步操作,还可能产生额外的随机读取。磁盘I/O再快,也比在内存里扫一个页要慢。
这种场景经常出现在配置表、字典表、枚举数据表上。比如一张类目表有300条记录,建了name索引,执行SELECT * FROM category WHERE name = '电子产品',EXPLAIN出来大概率还是ALL。这很合理,MySQL不傻,为了300行去读写索引完全是浪费。唯一要注意的是,这种全表扫描不会造成性能瓶颈,不要在它上面钻牛角尖,强行用FORCE INDEX反而会拖慢性能。
还有一个类似场景:MySQL有一个名为index的访问类型,它表示“扫描二级索引树”。这种情况和ALL不同,它不扫全表而扫索引,通常发生在查询的列恰好能被某个二级索引完全覆盖时,虽然访问类型不是理想的ref或range,但比ALL通常要快。很多人会把index误认为全表扫描,其实它是另一种优化路径。
2.3 统计信息不准,优化器被“错误情报”带偏了
很多有经验的DBA都遇到过这种诡异状况:同样一条SQL,昨晚跑还是走索引,今天跑突然全表扫描了,数据量和SQL可都没变。这种变化十有八九出在统计信息上。优化器做成本计算时依赖的不是实时扫描数据,而是存储在数据字典里的统计信息,比如表的行数、索引的基数(cardinality)、字段的选择度等。
如果一张表经常有大量增删改操作,但没有及时更新统计信息,比如走了大批量DELETE后表还是显示有上百万行,或者频繁UPDATE导致索引列的值的分布完全变了,可统计信息还停留在旧状态,优化器就会用错误的数据做成本估算,自然容易得出“走索引不如全表扫”的结论。
我遇到过最典型的一个案例:一张流水表每天凌晨批量插入几十万条数据,某个查询在白天走索引只要几十毫秒,到了下午突然变成了全表扫描,耗时直接涨到几秒。排查后发现,这张表的统计信息是前一晚凌晨批量导入前采样的,抽样时的数据分布和导入后的实际分布差距太大,导致优化器对索引过滤能力的预估严重偏低。执行一次`ANALYZE TABLE
