索引原理 Indexing
数据库⭐⭐⭐ 高级🔥🔥🔥 高频
💡 核心要点
MySQL InnoDB 使用 B+ 树作为索引数据结构,磁盘 I/O 友好且支持范围查询。索引能极大加速查询,但也有维护成本和失效场景。覆盖索引(无需回表)是优化查询性能的重要手段。
什么是索引?
索引是数据库中一种单独的、物理的数据结构,用于加速对表中数据的检索。没有索引的查询需要全表扫描(O(n)),有了索引可以优化到 O(log n)。
类比: 索引就像书的目录,能快速定位到指定页码,而不必从头翻到尾。
索引类型
按数据结构分类
| 类型 | 引擎支持 | 特点 |
|---|---|---|
| B+ 树索引 | InnoDB、MyISAM | 最常用,支持范围查询、排序 |
| 哈希索引 | Memory(InnoDB 自适应哈希) | 等值查询极快,不支持范围查询 |
| 全文索引 | InnoDB(5.6+)、MyISAM | 用于文本模糊搜索 |
| 空间索引 | MyISAM | 用于地理空间数据(R 树) |
按逻辑角色分类
| 类型 | 说明 |
|---|---|
| 主键索引 | 唯一标识每行数据,InnoDB 中也是聚簇索引 |
| 唯一索引 | 确保列值唯一,允许 NULL |
| 普通索引 | 最基本的索引,无唯一性限制 |
| 组合索引 | 多列组合,遵循最左前缀原则 |
| 覆盖索引 | 查询所需列都在索引中,无需回表 |
B+ 树结构与特点
为什么 MySQL 选择 B+ 树?
B 树 vs B+ 树的关键区别:
- B 树的内部节点也存储数据;B+ 树只在叶节点存储数据
- B+ 树叶节点之间通过双向链表连接,支持顺序遍历和范围查询
B+ 树示意(3 阶):
[30 | 60] ← 内部节点(只存键)
/ | \
[10|20] [40|50] [70|80] ← 内部节点
/ |\ ...
[1|5|9]─►[10|15|20]─►[25|30|35]─► ... ← 叶节点(存键+数据,双向链表)选择 B+ 树的原因:
- 减少磁盘 I/O: B+ 树内部节点不存数据,单个节点可存更多键,树更矮(通常 3-4 层),每次查询只需 3-4 次磁盘 I/O
- 支持范围查询: 叶节点链表使范围扫描(
BETWEEN、>、<)非常高效 - 查询稳定性: 所有查询都要到叶节点,时间复杂度稳定为 O(log n)
- 顺序访问优化: 叶节点顺序排列,全表扫描时磁盘顺序读效率高
为什么不用红黑树或跳表?
- 红黑树: 高度比 B+ 树高,百万数据时树高可能 20+,磁盘 I/O 次数多
- 跳表: 适合内存结构(Redis 用跳表实现 Sorted Set),但磁盘随机访问效率低
聚簇索引与非聚簇索引
聚簇索引 (Clustered Index)
InnoDB 中,数据本身按主键顺序存储在 B+ 树的叶节点,每张表只有一个聚簇索引。
主键索引 B+ 树叶节点:
┌──────────────────────────────┐
│ id=1 | name="Alice" | age=25 │ ← 完整行数据
│ id=2 | name="Bob" | age=30 │
│ id=3 | name="Carol" | age=28 │
└──────────────────────────────┘非聚簇索引 (Non-Clustered / Secondary Index)
辅助索引的叶节点只存储索引列的值和主键值,通过主键再查完整行,这个过程叫回表。
name 列辅助索引:
┌─────────────────────┐
│ "Alice" → id=1 │ ← 只存 name + 主键
│ "Bob" → id=2 │
│ "Carol" → id=3 │
└─────────────────────┘
↓ 回表
聚簇索引查完整行MyISAM 不区分聚簇/非聚簇: MyISAM 索引和数据分开存储,所有索引都是非聚簇的,叶节点存的是数据文件的物理地址。
覆盖索引 (Covering Index)
当查询的所有列都包含在一个索引中时,数据库可以直接从索引返回结果,无需回表。
-- 假设有索引 idx_name_age (name, age)
-- 覆盖索引查询(无需回表)
SELECT name, age FROM users WHERE name = 'Alice';
-- 非覆盖索引查询(需要回表取 email)
SELECT name, age, email FROM users WHERE name = 'Alice';EXPLAIN 验证: Extra 列出现 Using index 说明用了覆盖索引。
组合索引的最左前缀原则
组合索引 (a, b, c) 等效于建了三个前缀索引:(a)、(a, b)、(a, b, c)。
-- 可以用到索引
WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3
WHERE a = 1 AND b > 5 -- a 用索引,b 范围后的列不能用
-- 用不到索引
WHERE b = 2 -- 没有 a
WHERE b = 2 AND c = 3 -- 没有 a
WHERE c = 3 -- 没有 a, b索引失效场景
面试必考!以下情况会导致索引无法被使用,退化成全表扫描:
| 场景 | 示例 | 原因 |
|---|---|---|
| 左模糊查询 | LIKE '%keyword' | B+ 树按前缀排序,无法匹配后缀 |
| 对索引列运算 | WHERE age + 1 = 26 | 索引存储的是原值,运算后无法定位 |
| 对索引列使用函数 | WHERE YEAR(create_time) = 2024 | 同上,应改为范围查询 |
| 类型隐式转换 | WHERE phone = 13812345678(phone 是 varchar) | 触发类型转换,相当于函数操作 |
| OR 连接非索引列 | WHERE id = 1 OR name = 'Alice'(name 无索引) | 一个条件全表扫描,OR 合并后整体也全扫 |
| NOT IN / != | WHERE status != 1 | 范围不连续,优化器可能放弃索引 |
| 违反最左前缀 | 跳过组合索引首列 | 见上节 |
-- 函数失效的正确改法
-- 错误
WHERE YEAR(create_time) = 2024
-- 正确
WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01'
-- 类型转换的正确改法
-- 错误(phone 是 VARCHAR)
WHERE phone = 13812345678
-- 正确
WHERE phone = '13812345678'索引设计原则
- 选择性高的列优先建索引(唯一值多的列,如 user_id > status)
- 频繁出现在 WHERE、JOIN ON、ORDER BY、GROUP BY 的列
- 不要对频繁更新的列建过多索引(维护索引有写入开销)
- 尽量使用覆盖索引减少回表
- 组合索引把选择性高的列放前面,且考虑查询语句的最左前缀
常见误区
易错点
- 主键建议用自增整数,UUID 或随机主键会导致 B+ 树频繁分裂(页分裂),降低写入性能
- NULL 值是可以走索引的,MySQL 5.6+ 已优化,
IS NULL和IS NOT NULL在选择性合适时会走索引 - 索引不是越多越好,每个索引都会占用存储空间,并且在 INSERT/UPDATE/DELETE 时需要同步维护,增加写入开销
- EXPLAIN 的 type 列: system > const > eq_ref > ref > range > index > ALL,
ref及以上才算高效利用索引
Q1: 为什么 MySQL 使用 B+ 树?
从三个角度回答:
减少磁盘 I/O: B+ 树内部节点只存键不存数据,一个节点可以存放更多键,使树更矮(通常 3-4 层)。每次查询对应 3-4 次磁盘 I/O,而红黑树对百万数据树高 20+。
支持高效范围查询: B+ 树叶节点通过双向链表连接,范围查询只需找到起始节点,然后顺序遍历链表,非常高效。B 树的内部节点也存数据,范围查询需要中序遍历整棵树。
查询性能稳定: B+ 树所有查询都必须到达叶节点,时间复杂度恒为 O(log n)。B 树可能在内部节点命中,查询时间不稳定。
vs 哈希索引: 哈希索引等值查询 O(1),但不支持范围查询和排序,而且需要整个键参与哈希,无法支持最左前缀。
Q2: 什么是覆盖索引和回表?
- 回表: 通过辅助索引找到主键,再用主键在聚簇索引中查找完整行数据,需要两次 B+ 树查找
- 覆盖索引: 查询所需的所有列都在某个索引中,查索引即可,无需回表
如何避免回表: 为常用查询建立包含所有 SELECT 列的组合索引(索引覆盖 SELECT 列),用 EXPLAIN 验证出现 Using index。
Q3: 索引失效场景(背 6 个)
- 左模糊
LIKE '%xx'— 无法利用 B+ 树前缀排序 - 索引列做函数/运算 —
YEAR(date)、age+1等,应改为等值或范围 - 类型隐式转换 — varchar 列用整数比较,MySQL 隐式调用 CAST,相当于函数操作
- OR 连接非索引列 — OR 的任一分支全表扫描会影响整体
- 违反最左前缀 — 组合索引跳过首列
- 数据选择性太低 — 如 status 只有 0/1 两个值,优化器认为全表扫描更快
延伸阅读
- MySQL 官方文档 - Optimization and Indexes
- Use The Index, Luke — 数据库索引专项学习网站
- 《高性能 MySQL》第 5 章:索引优化