本文以 InnoDB 为默认存储引擎。索引的本质:用额外的空间和写入维护成本,换取更少的数据扫描、更少的磁盘 IO 和更稳定的查询性能。
1. 索引的物理基础:B+ 树
InnoDB 默认索引结构是 B+ 树。
B+ 树特点:
- 多叉平衡树,不是二叉树。
- 非叶子节点只存键值和子页指针,不存完整数据。
- 叶子节点存数据或主键值。
- 叶子节点之间用双向链表连接。
- 所有叶子节点深度一致,查询路径长度稳定。
InnoDB 页默认 16KB。非叶子节点扇出很大,因此 B+ 树高度通常只有 2 到 4 层。一次查询通常只需几次页访问,磁盘 IO 可控。
B+ 树适合数据库索引的核心原因:
- 范围查询高效:叶子链表可直接顺序扫描。
- 排序高效:数据在索引内有序。
- 磁盘友好:每次 IO 读取一个页,页内可放大量键值。
- 高度低:减少随机 IO 次数。
对比:
- 哈希索引:等值查询 O(1),范围查询和排序不支持。
- B 树:非叶子也存数据,扇出较小,范围扫描不如 B+ 树。
- 跳表:内存结构常见,磁盘数据库较少作为主索引。
2. InnoDB 的索引组织方式
2.1 聚簇索引
InnoDB 是索引组织表。聚簇索引即主键索引,叶子节点存储完整行数据。
如果表没有主键:
- InnoDB 选择第一个非空唯一索引作为聚簇索引。
- 如果没有合适的唯一索引,则生成 6 字节隐藏 row_id。
主键设计直接影响:
- 二级索引大小:二级索引叶子存主键值,主键越长,所有二级索引越大。
- 插入性能:随机主键容易造成页分裂。
- 查询性能:主键定位最快。
自增主键的优势是顺序插入,减少页分裂,提高页填充率。随机主键或无序 UUID 会导致中间插入、页分裂和空间碎片。
2.2 二级索引
二级索引也叫辅助索引。叶子节点不存完整行,而是存:
- 索引列值
- 主键值
查询过程:
- 在二级索引中定位到索引列条件。
- 拿到主键值。
- 回聚簇索引查完整行。
这个过程叫回表。
回表是随机 IO 的主要来源之一。二级索引扫描出的主键可能离散分布,回表时访问的聚簇索引页可能不连续。
2.3 联合索引
联合索引按定义列顺序排序。
例如索引 (a, b, c),排序规则是:
- 先按 a 排序
- a 相同按 b 排序
- b 相同按 c 排序
因此可用条件遵循最左前缀:
a = ?a = ? AND b = ?a = ? AND b = ? AND c = ?a = ? AND b > ?可用到 bb = ?无法直接使用该索引定位c = ?无法跳过 a、b 使用
范围条件后的列通常不能继续用于索引定位。例如 (a, b, c) 中:
sql
WHERE a = 1 AND b > 2 AND c = 3
a 和 b 可用于定位,c 不能用于缩小 B+ 树扫描范围。
3. 关键执行机制
3.1 回表
二级索引拿到主键后,再查聚簇索引。回表次数多,随机 IO 多,性能下降。
减少回表的方式:
- 使用覆盖索引。
- 只查询必要列。
- 调整查询条件,减少无效记录。
3.2 覆盖索引
查询需要的列全部在索引中,不需要回表。
例如索引 (a, b, c):
sql
SELECT a, b, c FROM t WHERE a = 1 AND b = 2;
该查询可被索引覆盖。EXPLAIN 的 Extra 显示 Using index。
InnoDB 二级索引叶子包含主键,因此:
sql
SELECT id, a, b FROM t WHERE a = 1;
如果 id 是主键,也可能被 (a, b) 覆盖。
覆盖索引减少回表,但不改变 MVCC 可见性判断。某些情况下仍需访问 undo 判断版本可见性。
3.3 自适应哈希索引
InnoDB 会为热点等值查询页建立自适应哈希索引,即 AHI。
特点:
- 自动维护,不可直接干预。
- 只适合等值查询。
- 范围查询仍走 B+ 树。
- 可能增加维护开销,高并发下有时被关闭。
3.4 页分裂与页合并
B+ 树页有容量限制。插入新记录时:
- 顺序插入:追加到页尾,页分裂少。
- 随机插入:可能插入已满页中间,触发页分裂。
- 页分裂会产生新页,影响填充率,增加碎片。
删除记录通常先标记删除,不一定立即合并页。大量删除后可能产生空间碎片。重建表可整理碎片。
4. 优化器与执行计划
MySQL 优化器基于成本选择索引。成本包括:
- IO 成本
- CPU 成本
- 扫描行数估算
- 回表代价
- 排序代价
优化器依赖统计信息:
- 索引基数 Cardinality
- 数据分布
- 页数量
统计信息可能不准。ANALYZE TABLE 可更新统计信息。统计不准时,优化器可能选错索引。
EXPLAIN 关键字段:
type:访问类型。常见从好到差:system、const、eq_ref、ref、range、index、ALL。key:实际使用的索引。rows:预估扫描行数。filtered:存储引擎返回后剩余条件过滤比例。Extra:Using index:覆盖索引。Using where:Server 层过滤。Using filesort:需要额外排序。Using temporary:使用临时表,常见于分组或去重。
常见索引未使用场景:
- 索引列上做函数或表达式:
WHERE DATE(created_at) = '2024-01-01'。 - 隐式类型转换:字符串列与数字比较。
- 前导模糊匹配:
LIKE '%abc'。 - 联合索引不满足最左前缀。
- OR 条件中部分列无索引。
- 低选择性列,优化器认为全表扫描更便宜。
- 范围过大,回表成本高于全表扫描。
5. 索引设计核心
5.1 联合索引列顺序
常见原则:
- 等值条件列放前面。
- 范围条件列放后面。
- 排序、分组列结合最左前缀考虑。
- 选择性高的列不一定无脑放最前,要结合查询模式。
例如查询:
sql
WHERE a = ? AND b = ? ORDER BY c
索引 (a, b, c) 可同时支持过滤和排序。
如果查询:
sql
WHERE a = ? AND b > ? ORDER BY c
索引 (a, b, c) 中,b 是范围,c 的全局有序性不能继续保证,排序可能仍需 filesort。
5.2 选择性
选择性 = 不重复值数量 / 总行数。越接近 1,过滤效果越好。
但单列选择性不是唯一标准。联合索引的选择性是组合结果。低选择性列放在联合索引前导,可能扫描大量记录。
5.3 前缀索引
对长字符串,可只索引前 N 个字符:
sql
KEY idx_name (name(10))
优点:索引更小。
限制:
- 无法覆盖完整列查询。
- 通常无法支持
ORDER BY name。 - 需要选择足够长前缀,使选择性接近完整列。
5.4 控制索引数量
索引不是越多越好。每个索引都带来:
- 插入、更新、删除时的维护成本。
- 存储空间。
- 优化器选择成本。
- 页分裂和碎片风险。
写多读少表尤其要控制索引数量。
5.5 主键设计
主键应尽量:
- 短。
- 有序。
- 稳定,不频繁更新。
自增 bigint 是常见选择。分布式场景可用有序雪花 ID。无序 UUID 作为聚簇索引会导致随机插入和二级索引膨胀。
6. 边界与误区
- 索引不改变数据可见性,MVCC 仍通过 undo 判断版本。
- 覆盖索引避免回表,但不一定避免所有版本判断。
type=index是全索引扫描,不等于高效。Using where不一定坏,Using filesort也不一定坏,需结合数据量和排序量。- 唯一索引等值查询在记录存在时,InnoDB 锁可能退化为记录锁;普通二级索引加锁还会涉及主键锁。
- 索引失效不是绝对规则,优化器基于成本决策。
force index可临时验证,但不应作为长期设计。
7. 总结
MySQL InnoDB 索引的核心链路:
- B+ 树提供有序、低高度、范围友好的索引结构。
- 聚簇索引叶子存完整行,主键即数据。
- 二级索引叶子存索引列和主键,查询常需回表。
- 联合索引遵循最左前缀,范围列后的列通常不能继续定位。
- 覆盖索引用于减少回表,降低随机 IO。
- 优化器基于统计和成本选索引,可能选错。
- 索引设计围绕查询路径:过滤、连接、排序、分组、覆盖、回表成本。