跳到主要内容

覆盖索引

问题

什么是覆盖索引?它是如何减少回表提升查询性能的?如何判断查询是否使用了覆盖索引?

答案

覆盖索引的概念

覆盖索引(Covering Index):当一个查询需要的所有列都包含在某个索引中,不需要回到聚簇索引(回表)就能获取数据,这个索引就称为覆盖索引。

核心价值

覆盖索引 不是一种索引类型,而是一种 索引使用策略——确保索引"覆盖"了查询所需的所有列。

回表 vs 覆盖索引

对比

项目有回表覆盖索引
B+ 树查找次数2 次(二级 + 聚簇)1 次(仅二级)
磁盘 IO
随机 IO有(回表是随机访问)
EXPLAIN ExtraUsing index

如何判断覆盖索引

使用 EXPLAIN 分析执行计划,关注 Extra 列:

-- 假设有索引 idx_name_age (name, age)

-- ✅ 覆盖索引 → Using index
EXPLAIN SELECT name, age FROM users WHERE name = '张三';
-- Extra: Using index

-- ❌ 需要回表(SELECT * 需要其他列)
EXPLAIN SELECT * FROM users WHERE name = '张三';
-- Extra: NULL(无 Using index)

-- ✅ 覆盖索引(主键隐含在二级索引中)
EXPLAIN SELECT id, name, age FROM users WHERE name = '张三';
-- Extra: Using index
为什么 id 也算覆盖

InnoDB 的二级索引叶子节点自动包含主键值(用于回表)。所以 (name, age) 索引实际存储的是 (name, age, id),查询 id, name, age 三列都能覆盖。

覆盖索引的典型应用

1. 列表查询优化

-- 商品列表页只需要展示名称和价格
-- 建立索引 (category_id, name, price)
SELECT name, price FROM products WHERE category_id = 10
ORDER BY name LIMIT 20;
-- Using index(覆盖索引 + 索引排序)

2. COUNT 优化

-- 索引 (status)
SELECT COUNT(*) FROM orders WHERE status = 'paid';
-- Using index(只需要在索引中统计数量,不需要行数据)

3. 判断存在性

-- 索引 (email)
SELECT 1 FROM users WHERE email = 'test@example.com' LIMIT 1;
-- Using index

4. 分页优化(延迟关联)

深分页时,覆盖索引 + 延迟关联能极大提升性能:

-- ❌ 深分页,需要回表获取 10 万 + 10 行的完整数据后丢弃前 10 万行
SELECT * FROM orders ORDER BY created_at DESC LIMIT 100000, 10;

-- ✅ 覆盖索引 + 延迟关联,先在索引中定位 ID,再查 10 行数据
SELECT o.* FROM orders o
INNER JOIN (
SELECT id FROM orders ORDER BY created_at DESC LIMIT 100000, 10
) AS t ON o.id = t.id;
-- 子查询用覆盖索引扫描,只回表 10 行

5. 索引中包含排序列

-- 索引 (status, created_at)
SELECT id, status, created_at FROM orders
WHERE status = 'pending'
ORDER BY created_at DESC;
-- Using index(覆盖 + 排序都在索引中)
-- 既不用回表,也不用 filesort

索引包含(INCLUDE)

PostgreSQL 和 SQL Server 支持 INCLUDE 列,专门为覆盖索引设计。

-- PostgreSQL:INCLUDE 列只在叶子节点存储,不参与索引排序
CREATE INDEX idx_status_include ON orders (status)
INCLUDE (name, amount);

-- 可以覆盖这个查询
SELECT name, amount FROM orders WHERE status = 'paid';

INCLUDE vs 联合索引

特性联合索引 (a, b, c)INCLUDE (a) INCLUDE (b, c)
索引排序键a, b, c 都参与只有 a 参与
索引大小较大(非叶也有 b, c)较小(b, c 只在叶子)
支持 WHERE b = ?✅(按最左前缀)
覆盖 SELECT b, c
MySQL 不支持 INCLUDE

MySQL(InnoDB)不支持 INCLUDE 语法。要实现覆盖索引,只能使用联合索引 (a, b, c) 把需要的列全都加进去。

覆盖索引的局限

局限说明
索引维护成本列越多,写入时维护成本越高
存储空间宽索引占用更多磁盘和内存
字段选择TEXT/BLOB 等大字段不适合加入索引
SELECT *很难覆盖所有列,应避免 SELECT *
变更频率高的列频繁更新的列加入索引会增加维护开销

设计覆盖索引的建议

  1. 分析高频查询:找出 QPS 最高的查询,优先为它们设计覆盖索引
  2. 避免 SELECT *:只查需要的列,给覆盖索引创造条件
  3. 联合索引复用:一个 (a, b, c) 索引能覆盖 a(a, b)(a, b, c) 的查询
  4. 主键免费:二级索引自动包含主键列,不需要重复加
  5. 权衡写入性能:索引列不宜过多,通常 3-5 列以内

常见面试问题

Q1: 什么是覆盖索引?有什么好处?

答案

覆盖索引是指查询所需的所有列都在索引中,不需要回表查聚簇索引。

好处:

  1. 减少 IO:只需一次索引查找,不需要回表
  2. 避免随机 IO:回表是根据主键随机访问数据页,覆盖索引避免了这一步
  3. 利用顺序 IO:索引中数据紧凑有序,可以利用顺序 IO
  4. 减少读取数据量:索引页比数据页小得多

Q2: EXPLAIN 中如何看到覆盖索引?

答案

EXPLAIN 的 Extra 列显示 Using index 表示使用了覆盖索引。注意区分:

  • Using index:覆盖索引,不需要回表
  • Using index condition:索引条件下推(ICP),仍需回表
  • Using where:在存储引擎层之上再过滤

Q3: SELECT * 为什么不好?

答案

从索引角度来讲:

  1. SELECT * 返回所有列,很难被覆盖索引覆盖,几乎一定要回表
  2. 传输无用数据浪费网络带宽
  3. 无法利用覆盖索引做深分页优化
  4. 如果表结构变化(加了列),可能导致应用异常

Q4: 覆盖索引和联合索引是什么关系?

答案

  • 联合索引是一种索引结构,包含多个列
  • 覆盖索引是一种使用策略,查询列都能被索引覆盖

通常通过创建联合索引来实现覆盖索引。例如 INDEX(a, b, c) 可以覆盖 SELECT a, b, c FROM t WHERE a = 1 的查询。

联合索引的设计需要同时考虑查询过滤条件(WHERE)、排序(ORDER BY)和返回列(SELECT),才能最大限度实现覆盖索引。

Q5: 如何用覆盖索引优化深分页?

答案

深分页 LIMIT 100000, 10 的问题在于需要回表 100010 行后丢弃前 100000 行。优化方法是"延迟关联":

-- 1. 子查询用覆盖索引只获取 ID
-- 2. 外层查询只回表 10 行
SELECT * FROM orders o
INNER JOIN (
SELECT id FROM orders
WHERE status = 'paid'
ORDER BY created_at DESC
LIMIT 100000, 10
) AS t ON o.id = t.id;

子查询在 (status, created_at) 索引上扫描 + 覆盖,不回表。最终只对 10 个 ID 回表获取完整数据。

相关链接