为什么加了索引查询还是慢:B+树、回表与执行计划的完整链路
很多人在表上加了索引,以为查询就一定快,结果一看执行计划还是全表扫描,或者走了索引却比不走还慢。问题往往不在索引本身,而在于把索引当成了加速开关。实际上索引只是一棵需要被正确使用的数据结构,查询快慢由优化器、数据分布和回表成本共同决定。
以 InnoDB 为例,索引的本质是 B+树。叶子节点按顺序连成链表,非叶子节点只存键和指针。聚簇索引的叶子存整行数据,二级索引的叶子只存索引列和主键。查询先在二级索引树里找到主键,再拿主键去聚簇索引树里找整行,第二步就叫回表。每回表一次,就是一次随机读,返回行数一多,随机读的成本就可能超过顺序全表扫描。
如果查询要的列刚好都在二级索引里,就不用回表,执行计划的 Extra 会出现 Using index,这叫覆盖索引。这就是为什么只取索引列会快很多倍,而 select 星号容易慢:不是索引变强了,而是少了一次到聚簇索引树的查找。写查询时先问一句,我真的需要整行吗,往往比加索引更有效。
MySQL 也不会无条件走索引。它会根据统计信息估算每条路径的成本:扫描行数、随机 IO、排序、回表。估算走索引要回表十万行,而全表扫描顺序读十万行更便宜时,它就会选全表扫描。统计信息过期、数据倾斜严重时,这个估算还会错,于是出现明明有索引却不用的假象。先怀疑统计信息和数据分布,再怀疑索引本身。
索引失效有几种典型情况。对索引列做函数、运算、隐式类型转换时,索引就用不上,比如 varchar 列用数字比较,因为 B+树的有序性被破坏了。LIKE 以百分号开头无法定位左边界,复合索引不满足最左前缀时后面的列也用不上,范围条件之后的列往往失效,OR 连接的条件如果有一边没有索引,也可能退化成全表扫描。这些不是玄学,都是左边界和有序性被破坏的结果。
排序和连接也受索引影响。索引叶子本来就是有序的,刚好能帮 ORDER BY 省掉 filesort。但如果 WHERE 和 ORDER BY 用的不是同一个索引,或者对排序列做了函数处理,还是要额外排序。连接时驱动表选错、被驱动表没有索引,就会变成对每行都全表扫描一次,数据量一放大就是灾难。这时问题不在单表索引,而在连接顺序和驱动表选择。
排查要先看 EXPLAIN,而不是凭感觉加索引。看 type 是 ref、range 还是 ALL,看 key 实际用了哪个索引,看 rows 估算多少行,看 Extra 有没有 Using filesort、Using temporary、Using index。type 从好到坏大概是 const、ref、range、index、ALL,但不要只看 type,要结合 rows 和回表量。再看慢查询日志的 Rows_examined 和 Rows_sent,如果 examined 远大于 sent,说明读了很多用不上的行。
修查询往往比加索引更重要。只取需要的列,避免 select 星号,避免对索引列做运算,把隐式转换改成显式同类型比较,把前导百分号的 LIKE 改成前缀或全文索引,复合索引按查询频率和选择性排好顺序,定期更新统计信息。每次改动都要对照强制索引前后的执行时间和 examined 行数验证,而不是凭感觉加一个索引就收工。
索引也不是免费的。每次写入都要维护所有 B+树,索引越多写越慢、空间越大,还会拖慢更新和删除。读多写少的表可以多建,写多读少的表要克制,长期没被用到的索引应该删掉,用慢日志和性能视图确认它真的没人用,而不是留着以防万一。
所以查询慢很少是单一原因,通常是索引结构、优化器成本估算、回表量、排序和连接共同作用的结果。理解 B+树叶子存什么、优化器按什么算成本、EXPLAIN 每个字段是什么意思,才能从加个索引试试,变成知道为什么慢、改哪里最划算。
- 点赞
- 收藏
- 关注作者
评论(0)