MySQL 覆盖索引怎么验:用 EXPLAIN ANALYZE 看 Using index 与回表耗时
同一条 SELECT 在仅 uid 二级索引与 (uid, amount, status) 覆盖索引下的 Extra / TREE / actual time 对比,附 MySQL 8.0.46 实测与可下载演示脚本。
回表不是玄学:Extra 为空往往在查二级索引后再回主键;覆盖后 Extra 变 Using index,TREE 写成 Covering index lookup,actual time 从约 2.7ms 掉到约 0.3ms。
适用对象
- 先看 Extra:空或 Using index condition 多半还要回表;Using index 才是覆盖命中。
- TREE 格式比传统表格更直白:Index lookup vs Covering index lookup。
- EXPLAIN ANALYZE 会真正执行语句,用它比只看 rows 估算更能确认耗时方向。
- 覆盖索引列要覆盖 SELECT 列表;多加列会放大写入与磁盘,按真实查询裁。
先造一条“会回表”的语句
表结构很简单:orders(id PK, uid, amount, status, created_at)。先只建 `INDEX idx_orders_uid (uid)`,再查 id/amount/status——uid 在索引里,后两列不在,必然回表。
传统 EXPLAIN 里 Extra 为空;TREE 格式写 `Index lookup on orders using idx_orders_uid`。optimizer cost 603,rows=1724。
然后换成 `INDEX idx_orders_uid_covering (uid, amount, status)`。主键 id 会附在二级索引叶子上,SELECT 列表都能从索引拿到,Extra 变成 `Using index`,TREE 变成 `Covering index lookup`,cost 降到 191。
同一语句、同一测试库的前后对比
EXPLAIN ANALYZE 跑了三轮(热缓存)。回表阶段 actual time 分别是 3.21 / 2.56 / 2.74 ms;覆盖后是 0.301 / 0.328 / 0.289 ms。大约一个数量级的差距。
这是笔记本 Docker、自造数据、单次测量,只说明方向,不是生产 SLA。生产上还要看写入放大、Online DDL 窗口和回滚方案。
| 指标 | 仅 idx(uid) | 覆盖 (uid, amount, status) |
|---|---|---|
| Extra | (空,需回表) | Using index |
| TREE 访问方式 | Index lookup | Covering index lookup |
| optimizer cost | 603 | 191 |
| actual time(三轮) | 3.21 / 2.56 / 2.74 ms | 0.301 / 0.328 / 0.289 ms |
| 返回行数 | 1724 | 1724 |
从怀疑回表到改索引的顺序
我每次都按这个顺序,避免一上来就加很宽的覆盖索引。
回表排查五步
- 01
01
固定语句与参数
先用业务真实参数,不要用“随便一个 uid”。
- 02
02
看 Extra / TREE
空 Extra 或 Index lookup → 怀疑回表;Using index → 已覆盖。
- 03
03
EXPLAIN ANALYZE
记录 actual time;写语句包在事务里回滚。
- 04
04
设计最小覆盖列
WHERE 等值列在前,SELECT 需要的列跟在后面;别把整行塞进索引。
- 05
05
Online DDL + 回测
大表用 ALGORITHM=INPLACE/INSTANT(视版本),改完再用同一条 EXPLAIN ANALYZE 对比。
演示脚本会 DROP/CREATE INDEX,只适合测试库;生产变更走你们自己的审批。
演示脚本与本次样例 CSV
演示 shell 会在指定库上重建两种索引并打印 EXPLAIN / EXPLAIN ANALYZE;样例 CSV 就是本文测试库的原始记录,可对照列含义。
本节判断
- 适用:MySQL 8.0 / 8.4(已在 8.0.46 测试)。
- 演示脚本会改索引,只在测试库跑。
- 数据:自造 shop.orders 20 万行;文中数字非客户数据。
参考依据
以下来源用于确认市场趋势、政策背景和术语边界;具体落地方案仍以客户的数据范围、权限和交付目标为准。
常见问题
为什么 SELECT 里有 id,覆盖索引却没把 id 写进列清单?
InnoDB 二级索引叶子带主键。SELECT id 通常仍可被覆盖;把 amount、status 放进索引才是消除回表的关键。
Using index condition 算覆盖吗?
不算完整覆盖。ICP 是在索引上先过滤,符合条件的行仍可能回表取剩余列。要看到 Using index(且 TREE 写 Covering index lookup)才是覆盖命中。
覆盖索引是不是越宽越好?
不是。列越多,写入越慢、索引越大。只覆盖高频、稳定的查询形状;报表类宽列查询更适合单独评估。