覆盖索引指查询所需所有字段均包含在索引中,无需回表查主键索引;它通过避免二次i/o提升性能,尤其利于宽表和大字段场景,但需合理设计联合索引顺序与列选择。

覆盖索引是什么,为什么它能跳过回表
覆盖索引不是新索引类型,而是查询恰好只用到索引中已有的列(包括 WHERE、ORDER BY、SELECT 所有字段),无需再回到主键索引(聚簇索引)查整行数据。MySQL/PostgreSQL 在执行计划里看到 Using index 就是命中了覆盖索引;如果出现 Using where; Using index,说明 WHERE 条件也走索引下推,效率更高。
- 回表是性能杀手:InnoDB 主键索引叶子节点存整行数据,二级索引叶子节点只存主键值;查完二级索引还得拿主键去主键索引里再查一次,I/O 翻倍
- 覆盖索引把“查索引 + 查数据”压缩成“只查索引”,尤其对宽表、大字段(
TEXT、BLOB)效果显著 - 不是所有 SELECT * 都能被覆盖——只要 SELECT 里出现索引没包含的列,立刻失效
怎么写覆盖索引:列顺序和冗余字段取舍
覆盖索引本质是为特定查询定制的联合索引,顺序必须匹配查询模式。例如查询 SELECT user_id, status, created_at FROM orders WHERE shop_id = ? AND status IN (?, ?) ORDER BY created_at DESC,推荐建:
CREATE INDEX idx_shop_status_created ON orders (shop_id, status, created_at) INCLUDE (user_id);
注意不同数据库语法差异:
- PostgreSQL 支持
INCLUDE子句,把user_id放进索引但不参与排序/查找,节省空间又满足覆盖 - MySQL 没有
INCLUDE,只能全塞进索引列:(shop_id, status, created_at, user_id)——但要注意,加在最后的user_id不参与范围查找,只用于覆盖;如果created_at是范围条件(如> '2024-01-01'),它后面的列就失效了 - 别把
SELECT *当目标:索引列越多越占空间、越拖慢 INSERT/UPDATE,优先保 WHERE 条件列 + ORDER BY 列 + 最常查的 1–2 个返回列
EXPLAIN 看不见的坑:WHERE 条件隐式类型转换毁掉索引
明明建了 (user_id, status) 索引,EXPLAIN 却显示 type: ALL 或 key: NULL?常见原因:
-
user_id是BIGINT,但查询写成WHERE user_id = '123'(字符串)→ MySQL 自动转类型,索引失效 -
status是ENUM或TINYINT,却用WHERE status = 'active'(而实际值是 1)→ 同样触发隐式转换 -
LIKE开头带通配符:WHERE name LIKE '%abc'无法用索引前缀,哪怕name是索引首列
验证方法很简单:
EXPLAIN FORMAT=TRADITIONAL SELECT ...;重点看
key 是否非空、rows 是否接近表总行数、有没有 Using filesort 或 Using temporary覆盖索引不是银弹:什么时候它反而更慢
覆盖索引对读友好,但对写有代价,且某些场景收益极低:
- 表更新频繁(每秒百次以上 INSERT/UPDATE):索引越多,维护成本越高;每个索引都要写磁盘、占 Buffer Pool,可能挤占热点数据缓存
- 查询返回字段极少但条件列本身就很宽:比如
WHERE long_json_col LIKE '%foo%',给这个列建索引意义不大,更别说覆盖 - 使用了函数或表达式:
WHERE YEAR(created_at) = 2024无法走created_at索引,自然也无法覆盖;得改成created_at >= '2024-01-01' AND created_at - 联合索引中存在高基数列(如
user_id)在前,低基数列(如status,只有 3 个值)在后:虽然能覆盖,但索引区分度差,B+ 树层级深,实际扫描行数未必少
真正要盯住的,是慢查询日志里 Rows_examined 远大于 Rows_sent 的那些语句——它们才最值得你动手建覆盖索引。










