一、索引的本质:有序的数据结构
索引的本质是什么?
索引是帮助MySQL高效获取数据的排好序的数据结构。
为什么需要排序?因为有序的数据结构能极大提升查找效率,就像查字典时的目录一样。
二、索引数据结构演进史
2.1 为什么不使用二叉树?
虽然二叉树查找时间复杂度为O(log n),但存在严重缺陷:
-
数据有序插入时,会退化为链表(O(n))
-
每个节点只存储一个键值,导致树高度过高,IO次数增多
2.2 红黑树的局限性
红黑树(平衡二叉树)解决了退化为链表的问题,但依然存在:
-
每个节点存储数据少,树高度仍然很高
-
大量数据时,树深度过深,磁盘IO次数过多
2.3 Hash表:快速但不万能
Hash表通过对key做一次hash计算直接定位数据位置,时间复杂度O(1)。
但致命缺陷:
-
仅支持等值查询(=、IN),不支持范围查询(>、<、between)
-
存在hash冲突问题
-
无法利用索引完成排序
2.4 B-Tree:多路平衡查找树
B-Tree的特点:
-
叶节点具有相同的深度
-
叶节点指针为空
-
节点中的数据索引从左到右递增排列
-
所有索引元素不重复
B-Tree的节点结构:[key|data|key|data|...]
每个节点既存储索引key,也存储对应的数据data。
2.5 B+Tree:MySQL的最终选择
B+Tree是B-Tree的变种,也是MySQL索引的默认数据结构。
B+Tree核心特点:
-
非叶子节点不存储data,只存储索引(冗余),可以存放更多索引
-
叶子节点包含所有索引字段
-
叶子节点用指针连接,形成双向链表,极大提升区间访问性能
B+Tree节点结构:
-
非叶子节点:
[key|pointer|key|pointer|...] -
叶子节点:
[key|data|next_pointer]
为什么选择B+Tree?
-
树高度更低:每个节点可存储更多key,减少磁盘IO次数
-
查询更稳定:所有查询都要走到叶子节点,时间复杂度稳定为O(log n)
-
范围查询高效:叶子节点双向链表结构,支持快速范围查询
-
适合磁盘存储:节点大小通常设置为页大小(16KB),减少磁盘IO
三、存储引擎的索引实现差异
3.1 MyISAM:非聚集索引
MyISAM存储引擎中,索引文件和数据文件是分离的:
-
.MYI文件存储索引 -
.MYD文件存储数据
索引结构中的data存储的是数据记录的地址指针。
3.2 InnoDB:聚集索引
InnoDB存储引擎采用聚集索引:
-
表数据文件本身就是按B+Tree组织的索引结构
-
叶子节点包含了完整的数据记录
-
主键索引的叶子节点存储整行数据
为什么InnoDB表必须建主键?
-
InnoDB的数据文件本身就是主键索引文件
-
如果没有显式定义主键,MySQL会自动选择:
-
第一个唯一非空索引
-
自动生成隐藏的row_id作为主键
-
为什么推荐使用整型自增主键?
-
节省空间:整型比字符串占用的存储空间小
-
查询高效:整型比较比字符串比较快
-
自增避免页分裂:插入时顺序写入,减少B+Tree结构调整
四、联合索引与最左前缀原理
4.1 联合索引存储结构
创建联合索引:
CREATE TABLE `employees` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(24) NOT NULL DEFAULT '' COMMENT '姓名',
`age` int(11) NOT NULL DEFAULT '0' COMMENT '年龄',
`position` varchar(20) NOT NULL DEFAULT '' COMMENT '职位',
`hire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间',
PRIMARY KEY (`id`),
KEY `idx_name_age_position` (`name`, `age`, `position`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='员工记录表';
联合索引的B+Tree结构:
-
按照
(name, age, position)的顺序构建 -
先按name排序,name相同按age排序,都相同按position排序
-
叶子节点存储这三个字段的值和主键id
4.2 最左前缀匹配原则
在MySQL 8.0之前,必须严格遵守最左前缀原则:
✅ 能使用索引的查询:
-- 使用name(最左列)
EXPLAIN SELECT * FROM employees WHERE name = 'Bill';
-- 使用name和age
EXPLAIN SELECT * FROM employees WHERE name = 'Bill' AND age = 31;
-- 使用name、age、position
EXPLAIN SELECT * FROM employees WHERE name = 'Bill' AND age = 31 AND position = 'dev';
❌ 不能使用索引的查询:
-- 缺少最左列name
EXPLAIN SELECT * FROM employees WHERE age = 30 AND position = 'dev';
-- 只有position
EXPLAIN SELECT * FROM employees WHERE position = 'manager';
五、MySQL 8.0的索引跳跃扫描(Index Skip Scan)
5.1 什么是索引跳跃扫描?
MySQL 8.0.13引入的优化特性,允许在某些情况下跳过联合索引的最左列。
官方示例:
CREATE TABLE t1 (f1 INT NOT NULL, f2 INT NOT NULL, PRIMARY KEY(f1, f2));
-- 插入数据...
EXPLAIN SELECT f1, f2 FROM t1 WHERE f2 > 40;
执行计划显示:
Extra: Using where; Using index for skip scan
5.2 索引跳跃扫描原理
MySQL优化器将查询重写为多个查询的UNION:
-- 原查询
SELECT f1, f2 FROM t1 WHERE f2 > 40;
-- 实际执行的查询(假设f1有1和2两个值)
SELECT f1, f2 FROM t1 WHERE f1 = 1 AND f2 > 40
UNION
SELECT f1, f2 FROM t1 WHERE f1 = 2 AND f2 > 40;
执行步骤:
-
获取联合索引第一列(f1)的所有不同值
-
为每个值构造查询:
f1 = value AND f2 > 40 -
执行每个查询并合并结果
5.3 使用场景与限制
适用场景:
-
联合索引第一列的不同值较少(低区分度)
-
查询条件中不包含第一列
限制条件:
-
只能用于单表查询,不能多表JOIN
-
不能使用GROUP BY或DISTINCT
-
查询字段必须都在索引中
-
第一列必须出现在索引中
性能考虑:
虽然跳跃扫描提供了便利,但不能依赖此优化。建立索引时仍应:
-
将区分度高、查询频繁的字段放在最左边
-
尽量避免在查询时省略最左列
六、为什么非主键索引叶子节点存储主键值?
-
保持数据一致性:当数据行更新时,只需更新主键索引,非主键索引无需改动
-
节省存储空间:存储主键值比存储整行数据小得多
-
避免数据冗余:减少存储空间占用和写入时的开销
七、Java开发中的索引优化实践
7.1 索引设计原则
-
为频繁查询的where条件字段创建索引
-
联合索引注意字段顺序:区分度高的在前
-
避免在索引列上使用函数或表达式
-
控制索引数量,避免过度索引
7.2 监控索引使用情况
-- 查看索引使用情况
SELECT * FROM sys.schema_unused_indexes;
-- 查看冗余索引
SELECT * FROM sys.schema_redundant_indexes;
7.3 常用优化技巧
-
使用覆盖索引,避免回表
-
利用索引下推(ICP)减少回表次数
-
注意索引失效场景(类型转换、函数计算等)
八、总结
MySQL索引的核心是B+Tree数据结构,它平衡了查询效率和存储成本。InnoDB的聚集索引设计让主键查询极为高效,而联合索引的最左前缀原则是SQL优化的关键点。
MySQL 8.0的索引跳跃扫描虽然打破了最左前缀的绝对限制,但只是特定场景下的优化手段,不能替代良好的索引设计。