回表可一眼通过EXPLAIN的Extra字段识别:含Using index表示覆盖索引、未回表;仅有Using where(无Using index)即为回表铁证;JOIN中覆盖索引仅对被驱动表有效。

逻辑读高,大概率是因为 JOIN 过程中反复回表——每次从二级索引拿到主键后,还得跳去聚簇索引里捞整行数据,这一步在 EXPLAIN 里看不到显式语句,但 Extra 出现 Using where 而没有 Using index 就是铁证。
EXPLAIN 中怎么一眼识别回表?
看 Extra 字段是否含 Using index:
- 有
Using index:说明走的是覆盖索引,没回表 - 只有
Using where或Using where; Using join buffer:基本已回表 - 出现
Using filesort或Using temporary:覆盖索引救不了,得先解决排序/分组逻辑
注意:type=ref 或 eq_ref 只代表索引被用了,不代表覆盖;rows 小但 filtered 低,也常意味着回表量大。
JOIN 场景下覆盖索引只对被驱动表有效
MySQL 的 Nested Loop Join 是拿驱动表一行,去被驱动表里查匹配行。所以驱动表仍需访问完整数据(除非它本身用主键查询),但被驱动表可以靠覆盖索引省掉回表。
- 例如
SELECT o.id, o.status, u.name FROM orders o JOIN users u ON o.user_id = u.id WHERE o.status = 'paid' -
orders是驱动表,user_id必须有索引(否则全表扫);但u.name要避免回表,就得在users上建INDEX idx_user_cover (id, name) - 顺序不能错:
id必须放最左,因为ON u.id = o.user_id是等值匹配,必须命中最左前缀
为什么 SELECT * 会让覆盖索引失效?
因为 SELECT * 强制要求所有字段,而二级索引叶子节点只存「索引列 + 主键」,哪怕你给 users 建了 (id, name) 索引,只要查询里包含 email 或 created_at 这类非索引列,就必然触发回表。
- 错误写法:
SELECT u.* FROM orders o JOIN users u ON o.user_id = u.id - 正确写法:
SELECT u.id, u.name FROM orders o JOIN users u ON o.user_id = u.id,配合INDEX (id, name) - 别信“我加了所有字段进索引”——
VARCHAR(255)列全量进索引会导致页分裂、B+ 树变高,IO 反而更差
ORDER BY 和函数会让覆盖索引“形同虚设”
就算 SELECT 和 WHERE 全部字段都进了索引,一旦 ORDER BY 字段不在索引里,或者 SELECT 里用了函数,覆盖就破功。
-
SELECT u.name FROM users u WHERE u.id = 123 ORDER BY u.email→ 即使有(id, name)索引,email不在索引中,就会Using filesort -
SELECT UPPER(u.name) FROM users u WHERE u.id = 123→ 函数导致索引无法直接比对,即使name在索引里也白搭 -
IS NULL过滤要小心:如果字段定义为NOT NULL,WHERE u.name IS NULL会直接放弃索引
真正难的不是建索引,而是让业务 SQL 和索引设计咬合——字段选哪些、顺序怎么排、有没有隐式转换,每一步都会让 Using index 消失不见。










