一、什么是覆盖索引

1.1 回表查询

-- 表结构
CREATE TABLE users (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    age INT,
    email VARCHAR(100)
);
CREATE INDEX idx_name ON users(name);

-- 查询
SELECT * FROM users WHERE name = '张三';

查询过程

  1. 在idx_name索引找到name=‘张三’,获取id=10
  2. 回表:到主键索引查找id=10的完整数据
  3. 返回结果

IO次数:2次(索引1次 + 回表1次)

1.2 覆盖索引

-- 查询
SELECT id, name FROM users WHERE name = '张三';

查询过程

  1. 在idx_name索引找到name=‘张三’
  2. 索引中已包含id和name
  3. 直接返回,无需回表

IO次数:1次(只查索引)

覆盖索引:查询的列都在索引中,无需回表。


二、覆盖索引的优势

2.1 减少IO

-- 无覆盖索引
SELECT * FROM users WHERE name = '张三';
-- IO:索引 + 回表 = 2次

-- 覆盖索引
SELECT id, name FROM users WHERE name = '张三';
-- IO:索引 = 1次

2.2 提升性能

-- 测试
-- 100万数据,查询10000次

-- SELECT *(需要回表)
-- 耗时:5秒

-- SELECT id, name(覆盖索引)
-- 耗时:0.5秒

-- 性能提升:10倍

三、如何实现覆盖索引

3.1 主键索引天然覆盖

-- 主键索引是聚簇索引,包含完整数据
SELECT * FROM users WHERE id = 10;  -- 覆盖索引

3.2 设计联合索引

-- 查询
SELECT id, name, age FROM users WHERE name = '张三';

-- 索引设计
CREATE INDEX idx_name_age ON users(name, age);
-- 索引包含:name, age, id(主键自动包含)
-- 覆盖查询所需列

3.3 添加冗余列

-- 查询
SELECT user_id, order_no, status FROM orders WHERE user_id = 123;

-- 索引设计
CREATE INDEX idx_user_no_status ON orders(user_id, order_no, status);
-- order_no和status可能不在WHERE中,但在SELECT中
-- 添加到索引实现覆盖

四、EXPLAIN中的覆盖索引

4.1 Using index

EXPLAIN SELECT id, name FROM users WHERE name = '张三';

结果

Extra: Using index  ← 覆盖索引

4.2 完整示例

-- 无覆盖索引
EXPLAIN SELECT * FROM users WHERE name = '张三';
-- Extra: NULL(需要回表)

-- 覆盖索引
EXPLAIN SELECT id, name FROM users WHERE name = '张三';
-- Extra: Using index(覆盖索引)

五、覆盖索引的设计技巧

5.1 分析查询模式

-- 统计常用查询
-- 查询1(80%)
SELECT id, user_id, status FROM orders WHERE user_id = ?;

-- 查询2(15%)
SELECT id, user_id, created_at FROM orders WHERE user_id = ?;

-- 索引设计
CREATE INDEX idx_user_id_status_created ON orders(user_id, status, created_at);
-- 覆盖95%的查询

5.2 平衡索引大小

-- ❌ 差:索引过大
CREATE INDEX idx_all ON users(id, name, age, email, phone, address, ...);
-- 索引占用空间大,维护成本高

-- ✅ 好:只包含常用列
CREATE INDEX idx_common ON users(name, age, email);
-- 覆盖90%查询,索引大小合理

5.3 利用主键

-- 二级索引自动包含主键
CREATE INDEX idx_name ON users(name);
-- 实际包含:(name, id)

-- 查询
SELECT id, name FROM users WHERE name = '张三';
-- 覆盖索引(id自动包含)

六、实战案例

案例1:订单列表优化

-- 原查询(需要回表)
SELECT id, order_no, user_id, status, total_amount, created_at
FROM orders
WHERE user_id = 123
ORDER BY created_at DESC
LIMIT 20;

-- 原索引
CREATE INDEX idx_user_id ON orders(user_id);
-- Extra: NULL(需要回表)

-- 优化:覆盖索引
CREATE INDEX idx_user_date_no_status_amount ON orders(
    user_id,
    created_at,  -- ORDER BY列
    order_no,    -- SELECT列
    status,      -- SELECT列
    total_amount -- SELECT列
);
-- Extra: Using index(覆盖索引)

-- 性能提升:5倍

案例2:用户搜索优化

-- 查询
SELECT id, name, age FROM users
WHERE name LIKE '张%'
ORDER BY age;

-- 索引设计
CREATE INDEX idx_name_age ON users(name, age);
-- 覆盖id(主键)、name、age
-- Extra: Using index

案例3:统计查询优化

-- 查询
SELECT COUNT(*), SUM(amount) FROM orders WHERE status = 'completed';

-- 索引设计
CREATE INDEX idx_status_amount ON orders(status, amount);
-- 覆盖status和amount
-- Extra: Using index

七、覆盖索引的限制

7.1 SELECT *无法覆盖

-- SELECT *无法使用覆盖索引(除非主键查询)
SELECT * FROM users WHERE name = '张三';
-- 必须回表

建议:避免SELECT *,明确指定需要的列。

7.2 索引列太多

-- ❌ 差:索引过大
CREATE INDEX idx_many ON table(a, b, c, d, e, f, g, h);
-- 维护成本高

-- ✅ 好:合理数量
CREATE INDEX idx_few ON table(a, b, c);

7.3 TEXT/BLOB列

-- TEXT/BLOB列无法完整加入索引
CREATE INDEX idx_content ON articles(content);  -- 报错或需要前缀

-- 只能前缀索引
CREATE INDEX idx_content ON articles(content(100));
-- 无法完全覆盖

八、覆盖索引 vs 回表对比

对比项覆盖索引回表查询
IO次数1次2次
查询速度
索引大小较大较小
适用场景高频查询低频查询

九、总结

核心要点

  1. 覆盖索引:查询列都在索引中,无需回表
  2. 判断标志:EXPLAIN Extra: Using index
  3. 实现方法:联合索引包含所有查询列
  4. 性能提升:减少IO,提升查询速度
  5. 设计原则:平衡性能和存储,避免过大索引
  6. 主键优势:二级索引自动包含主键

记忆口诀

覆盖索引不回表,查询列全在索引中,
减少IO速度快,Using index是标志。
联合索引来实现,SELECT列全加上,
主键自动被包含,性能提升好几倍。
避免SELECT星号,索引大小要平衡。

本文字数:约2,300字 难度等级:⭐⭐⭐⭐(索引进阶)