深入理解索引的底层数据结构、分类与使用策略,学会用 EXPLAIN 分析查询性能。
一、为什么需要索引
场景: 一个 100 万行的用户表,执行 SELECT * FROM users WHERE email = 'test@example.com'。没有索引时,数据库必须逐行扫描全表(全表扫描),耗时数秒甚至更久。加了索引后,毫秒级返回。
本质: 索引是数据库维护的额外的数据结构 ,提供快速定位数据的能力,以写入性能换读取性能。
sql
-- 创建索引
CREATE INDEX idx_email ON users(email);
-- 复合索引
CREATE INDEX idx_name_age ON users(name, age);
二、索引数据结构 ------ B+ 树
2.1 B+ 树核心特性
| 特性 | 说明 |
|---|---|
| 多路平衡树 | 非二叉,每个节点可存大量键值对 |
| 非叶子节点存键 | 仅用于导航,不存数据,一个节点可包含数百个键 |
| 叶子节点存数据/指针 | 所有实际数据(或指向数据的指针)都在叶子节点 |
| 叶子节点链表连接 | 叶子节点通过双向链表串联,支持范围查询极速遍历 |
| 树高通常 3-4 层 | 百万级数据也只需 3-4 次磁盘 IO |
B+ 树为何适合数据库?
- 矮胖结构 → 减少磁盘 IO(树的高度 ≈ 磁盘寻道次数)
- 叶子链表 → 范围查询不用回溯(
BETWEEN、>、ORDER BY效率高) - 非叶子只存键 → 一页能装更多键,进一步降低树高
2.2 B+ 树 vs B 树 vs 哈希索引
| 类型 | 范围查询 | 等值查询 | 排序 | 适用场景 |
|---|---|---|---|---|
| B+ 树 | ✅ 极快 | ✅ 快 | ✅ 天然有序 | 大多数场景(MySQL InnoDB 默认) |
| B 树 | ❌ 慢 | ✅ 快 | ❌ | 已淘汰 |
| 哈希索引 | ❌ 不可用 | ✅ 最快 O(1) | ❌ | 精确等值查找(MEMORY 引擎) |
三、聚簇索引 vs 非聚簇索引
3.1 聚簇索引(Clustered Index)
- InnoDB 存储引擎的默认主键索引
- 叶子节点直接存储整行数据
- 一张表只能有一个聚簇索引
- 主键即聚簇索引(不设主键时,MySQL 会选唯一索引或隐式生成 row_id)
sql
-- 主键就是聚簇索引
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT, -- 聚簇索引
name VARCHAR(50),
email VARCHAR(100)
);
3.2 非聚簇索引(Secondary Index / 二级索引)
- 叶子节点存储的是主键值,而非整行数据
- 一张表可有多个非聚簇索引
- 查询时需回表(先查二级索引拿到主键 ID,再回聚簇索引拿整行数据)
sql
-- 二级索引
CREATE INDEX idx_email ON users(email);
-- 查询:SELECT * FROM users WHERE email = '...';
-- 流程:二级索引 → 拿到 id → 聚簇索引 → 拿到整行
3.3 覆盖索引(Covering Index)
- 要查询的列全部在二级索引中,无需回表
- 性能接近聚簇索引,是常见的优化手段
sql
-- 如果只需查询 id 和 email
-- 二级索引 idx_email 已包含(email, id),无需回表
SELECT id, email FROM users WHERE email = 'test@example.com';
经验原则: 设计索引时尽量包含 SELECT 的字段,减少回表次数。
四、复合索引与最左前缀原则
4.1 最左前缀原则
创建复合索引 (a, b, c) 时,实际生成的是以下组合的索引:
| 查询条件 | 是否命中索引 |
|---|---|
WHERE a = 1 |
✅ 命中 |
WHERE a = 1 AND b = 2 |
✅ 命中 |
WHERE a = 1 AND b = 2 AND c = 3 |
✅ 命中 |
WHERE b = 2 |
❌ 未命中(跳过了 a) |
WHERE a = 1 AND c = 3 |
⚠️ 部分命中(a 生效,c 不生效) |
WHERE b = 2 AND c = 3 |
❌ 未命中(跳过了 a) |
关键口诀: 带头大哥不能死,中间兄弟不能断。
sql
-- 创建复合索引
CREATE INDEX idx_status_created ON orders(status, created_at);
-- ✅ 命中:status 在最左
SELECT * FROM orders WHERE status = 'paid';
-- ✅ 完全命中
SELECT * FROM orders WHERE status = 'paid' AND created_at > '2024-01-01';
-- ❌ 未命中:跳过了 status
SELECT * FROM orders WHERE created_at > '2024-01-01';
4.2 复合索引设计建议
- 选择性高的列放在最左边(如 status 只有 3 种值 → 选择性低;email 唯一 → 选择性高)
- 经常作为范围查询的列放右边(因为范围条件后无法使用后续列)
- 不要建太多单列索引,尽量合并成复合索引
五、索引失效的常见场景
sql
-- ❌ 场景1:对索引列使用函数
WHERE YEAR(created_at) = 2024; -- 索引失效
WHERE created_at >= '2024-01-01' -- 索引生效(改为范围查询)
-- ❌ 场景2:隐式类型转换
WHERE phone = 13800138000; -- phone 是 VARCHAR,传入数字 → 索引失效
WHERE phone = '13800138000'; -- 正确
-- ❌ 场景3:LIKE 以通配符开头
WHERE name LIKE '%张三'; -- 索引失效
WHERE name LIKE '张三%'; -- 索引生效(前缀匹配)
-- ❌ 场景4:OR 条件中有一方无索引
WHERE email = 'a@b.com' OR phone = '123456'; -- phone 无索引则全表扫描
-- ❌ 场景5:NOT IN / != / IS NOT NULL
WHERE status != 'deleted'; -- 可能不走索引(取决于数据分布)
WHERE status = 'active'; -- 走索引
-- ❌ 场景6:复合索引违反最左前缀
-- 复合索引 idx(a,b,c),以下查询 不生效:
WHERE b = 1 AND c = 2; -- 跳过了 a
六、EXPLAIN 查看执行计划
6.1 基础用法
sql
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
6.2 关键字段解读
| 字段 | 含义 | 好 | 坏 |
|---|---|---|---|
type |
访问类型 | const > ref > range > index > ALL |
|
possible_keys |
可能使用的索引 | 非空 | - |
key |
实际使用的索引 | 非空 | NULL(未使用索引) |
rows |
扫描行数估计 | 越小越好 | 大 |
Extra |
额外信息 | Using index(覆盖索引) |
Using filesort、Using temporary |
6.3 实际分析示例
sql
-- 创建一个测试表
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
status VARCHAR(20),
amount DECIMAL(10,2),
created_at DATETIME,
INDEX idx_status (status),
INDEX idx_user_status (user_id, status)
);
-- 分析查询
EXPLAIN SELECT id, user_id, amount
FROM orders
WHERE user_id = 1001 AND status = 'paid'\G
输出解读:
type: ref--- 使用非唯一索引查找key: idx_user_status--- 使用了复合索引rows: 1--- 预估只扫描 1 行Extra: NULL--- 仍需回表查询 amount 字段
sql
-- 优化:改为覆盖索引
DROP INDEX idx_user_status ON orders;
CREATE INDEX idx_user_status_amount ON orders(user_id, status, amount);
EXPLAIN SELECT id, user_id, amount
FROM orders
WHERE user_id = 1001 AND status = 'paid'\G
-- Extra: Using index ← 覆盖索引,无需回表!
今日小结
- B+ 树 是 InnoDB 默认索引结构,矮胖 + 叶子链表使其同时擅长等值和范围查询
- 聚簇索引 存整行数据,一张表只有一个;二级索引 存主键值,需要回表
- 覆盖索引 是性能利器 --- 索引包含所有查询列,避免回表
- 最左前缀原则 是复合索引的核心,设计时把高选择性的列放左边
- 避免函数、隐式转换、LIKE 前缀通配符等导致索引失效的操作
- EXPLAIN 是 SQL 优化入门必修课,关注
type、key、rows、Extra四个字段