在MySQL中,一条SQL从客户端发到服务端后,会依次经过连接器、分析器、优化器和执行器。其中优化器负责决定走哪个索引、是否使用临时表、join顺序等。索引选择直接决定了扫描行数和是否回表,而回表又影响着磁盘IO模式。弄清楚这两个机制,才能解释为什么有的查询明明建了索引还是很慢。

一、优化器如何进行索引选择
MySQL优化器基于成本模型来选索引。它会估算全表扫描和各个索引扫描的IO成本与CPU成本,取总成本最小的方案。统计信息主要来自innodb_index_stats和innodb_table_stats,其中每个索引的基数(cardinality)代表不重复值的数量,基数越高通常过滤性越好。
比如一张用户表有索引idx_age和idx_name,查询条件是age=20 AND name='tom'。优化器会看两个索引的区分度,如果idx_name基数更大,大概率选它。但统计信息过期时可能误判,这时可用ANALYZE TABLE重新采样。另外,当查询需要排序且索引本身有序时,优化器也会倾向使用该索引以避免filesort。
我们可以通过EXPLAIN观察key字段确认实际使用的索引,rows字段是预估扫描行数。若发现选错索引,可用FORCE INDEX干预,但更推荐修正统计信息或调整索引设计。
EXPLAIN SELECT * FROM user WHERE age=20 AND name='tom'; -- 输出中 key 列显示实际选用的索引 -- rows 列显示预估需要扫描的行数 ANALYZE TABLE user; -- 重新统计表和索引信息,帮助优化器准确估算
二、什么是回表操作
InnoDB的表数据本身存放在聚簇索引(主键索引)的叶子节点中,二级索引的叶子节点存的是主键值而不是整行数据。当使用二级索引查询,而SELECT列表或WHERE条件里包含不在该二级索引中的列时,存储引擎必须先用二级索引拿到主键,再拿主键去聚簇索引查找完整行,这一步就是回表。
回表最大的问题是产生大量随机IO。二级索引扫描是顺序或范围读,而回表是根据主键离散访问聚簇索引,在机械盘上尤其慢。如果查询命中几千行,就可能发生几千次随机读。这也是为什么SELECT *配合二级索引常常性能糟糕。
判断是否回表可看EXPLAIN的Extra列。如果出现Using index表示覆盖索引,没有回表;如果出现Using where且没Using index,一般就发生了回表。下面例子用覆盖索引避免了回表。
-- 假设表 user 有二级索引 idx_name(name) -- 会发生回表,因为查了 * SELECT * FROM user WHERE name='tom'; -- 覆盖索引,不需要回表 SELECT name FROM user WHERE name='tom'; -- 联合索引避免回表 -- 建索引 idx_name_age(name, age) SELECT name, age FROM user WHERE name='tom';
三、索引选择与回表的关联影响
优化器选索引时并不单独考虑回表,而是把回表带来的随机IO算进总成本。如果某个二级索引过滤性好但-select列不全,回表成本高;另一个索引过滤性差但能覆盖查询,可能反而更优。这种权衡在宽表上特别明显。
实践中,我们常建联合索引来同时解决过滤和覆盖问题。比如查询频繁按status过滤并取update_time,建(status, update_time)联合索引既利于选择又避免回表。但要注意联合索引字段顺序,过滤性高的放前面通常更好。
此外,优化器可能因误判基数而选了会导致严重回表的索引。此时除了更新统计信息,也可考虑改写SQL,比如用子查询先限定主键范围再join,强制走覆盖索引路径。
-- 改写前:可能选错索引并大量回表 SELECT * FROM orders WHERE status=1 ORDER BY create_time LIMIT 100; -- 改写后:先覆盖索引拿主键,再回表少量数据 SELECT o.* FROM orders o JOIN (SELECT id FROM orders WHERE status=1 ORDER BY create_time LIMIT 100) t ON o.id = t.id;
四、总结与调优建议
理解MySQL的索引选择与回表,核心在于明白优化器的成本视角和InnoDB的物理存储结构。选索引不是选区分度最高的就行,还要看是否覆盖查询列。回表是二级索引查询的常见代价,应通过设计覆盖索引、合理建联合索引来规避。
线上调优时,先用EXPLAIN看key、rows、Extra,确认是否有Using index。统计信息过期就Analyze,索引缺失就补联合索引。避免习惯性写SELECT *,只取需要的字段往往就能少一次回表,查询延迟可能下降一个数量级。
| 现象 | 可能原因 | 应对手段 |
|---|---|---|
| 二级索引查询慢 | 大量回表随机IO | 建覆盖索引或联合索引 |
| 选错索引 | 统计信息不准 | ANALYZE TABLE |
| Extra无Using index | 查询列不在索引中 | 减少SELECT字段或扩索引 |