深入理解MySQL数据库索引

1. 索引简介
1.1 索引是什么?
MySQL的索引是一种数据结构 ,它可以帮助数据库高效地查询、更新数据表中的数据。索引通过一定的规则排列数据表中的记录,使得对表的查询可以通过对索引的搜索来加快速度。
MySQL索引类似于书籍的目录,通过指向数据行的位置,**可以快速定位和访问表中的数据。**就像使用汉语字典的目录(索引)页,可以通过笔画、偏旁部首、拼音等排序的目录快速查到所需要的字。
1.2 为什么要使用索引?
- 在工作或者学习中,如果对索引比较了解,可以写出更高效率的SQL语句。
- 索引可以提升数据检索的效率,在应用程序的运行过程中,查询操作的频率远远高于增删改的频率。
2. 索引需要采用什么样的数据结构?
2.1 Hash -- 哈希表
查询和删除数据的时间复杂度都是O(1),查询速度非常快,但MySQL并没有选择其作为索引的原因是Hash不支持范围查找。
2.2 二叉搜索树
落选原因:
- 最坏情况下,查询时间复杂度为O(N)。
- 节点个数过多无法保证数的高度。
- AVL和红黑树虽然是平衡或者近似平衡,但毕竟是二叉树。
- 由于数据都是在磁盘上保存的,在检索数据时,每次访问某个节点的子节点时都会发生一次磁盘IO,而在整个数据库系统中,IO是性能的瓶颈,减少IO次数可以有效提升性能。
2.3 N叉树
为了解决树高的问题,可以使用N叉树。
Tips:推荐一个数据结构可视化网站 :https://www.cs.usfca.edu/~galles/visualization/Algorithms.html。
由上图可知,相同数据量的情况下,N叉树的树高可以得到有效控制,也就意味着在相同的情况下可以减少IO的次数,从而提升效率。但是MySQL认为N叉树作为索引的数据结构不是最理想的。
2.4 B+树
-
MySQL使用的是B+树的升级版 ,一般B+树的叶子节点可以组织成一个单链表,MySQL则将其升级为双向链表结构 ,更大提升查询和修改的效率。
-
B+树是一种经常用于数据库和文件系统等场合的平衡查找树 ,MySQL索引采用的数据结构,以4阶B+树为例:
B+树的特点:
- 能够保持数据稳定有序 ,插入和修改有较为稳定的时间复杂度。
- 非叶子节点仅具有索引作用,不存储数据,所有叶子节点保存真实数据。
- 所有的叶子节点构成一个有序链表。
B+树和B树的对比:
- B+树的叶子节点中的数据是连续的,且相互链接,便于区间查找和搜索。
- 非叶子节点的值都包含在叶子节点中。
- 对于B+树而言,在和B树相同树高的情况下,查找任意元素的时间复杂度都为log(N),性能更加均衡。
3. MySQL中的页
3.1 为什么要使用页?
在.ibd文件中最重要的结构体就是Page(页),页是内存与磁盘交互的最小单元 ,默认大小为16KB,每次内存和磁盘的交互至少读取一页 ,所以在磁盘中每个页内部的地址都是连续的。根据局部性原理,将来要使用的数据大概率与当前访问的数据在空间上是邻近的,所以一次从磁盘中读取一页的数据放入内存中,当下次查询的数据还在这个页中就可以直接从内存中直接读取,从而减少磁盘IO,提高性能。
每一页中即使没有数据也会使用16KB的存储空间,同时与索引的B+树中的节点对应,查看页的大小,可以通过系统变量innodb_page_size
查看:
sql
mysql> show variables like 'innodb_page_size';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| innodb_page_size | 16384 | # 16384表示的是字节数 16*1024 = 16384
+------------------+-------+
1 row in set, 6 warnings (0.01 sec)
- 在MySQL中有多种不同类型的页,最常用的就是用来存储数据和索引的"索引页",也叫做"数据页",但是不论哪一种类型的页都会包含页头和页尾 ,页的主体信息使用数据行进行填充,数据页的基本结构如下图所示:
3.2 页文件头和页文件尾

- 着重关注内容:上一个页号和下一页页号,通过这两个属性可以把页和页之间链接起来,形成一个双向链表。
3.3 页主体
-
页主体部分是保存真实数据的主要区域,每当创建一个新页时,都会自动分配两个行,一个是页内最小行
Infimun
,另一个是页内最大行Supremun
,这两个行并不存储任何真实信息,而是作为数据行链表的头和尾。第一个数据行有一个记录下一行的地址偏移量的区域next_record
,将页内所有数据行组成了一个单向链表。
-
当向一个新页插入数据时,将
Infimun
连接第一个数据行,最后一行真实数据行连接Supremun
,通过这样的方式,数据行就构成了一个单向链表的结构,更多的数据插入之后,会按照主键从小到大的顺序进行连接。
3.4 页目录
-
当按照主键或者索引查找某条数据的时候,最简单直接的方案是从最小行
Infimun
开始,沿着链表顺序逐个比对查找,但一个页有16KB,通常会存在上百行数据,每次都要遍历数百行,无法满足高效查询。为了提高查询效率,InnoDB采用二分查找解决上述问题。 -
具体实现方式是在每一个页加入一个页目录,将页内包含最小、最大行在内的所有行进行分组,约定头行单独为一组,其他每个组最多8条数据,同时把每一个组最后一行在页中的地址,按照主键从小到大记录的顺序记录在页目录中,页目录中每一个位置称为一个槽,每个槽对应一个分组。
-
后续查找按照先在页目录中查找到对应的槽,再进行遍历槽中的数据,最终得到想要的结果。
3.5 数据页头
数据页头记录当前页保存数据相关的信息。
4. B+树在MySQL索引中的应用

查找id=5
的过程:加载索引页1 -> 加载索引页2 -> 加载数据页3。
注意点:
- 因为索引页数据量相对较小的情况下,如果可以把索引页缓存到内存中 ,一次IO就可以找到目标数据,提升查询性能。
5. 索引分类
5.1 主键索引
-
当在一个表上定义一个主键时,InnoDB使用其作为聚集索引。
-
推荐:为每一个表定义一个主键,如果没有逻辑上唯一且非空的列或者列集可以使用主键,则添加一个自增列。
创建主键索引:
sql
-- 方式一:创建表时创建主键
create table t_test_pk(
id bigint primary key auto_increment,
name varchar(20)
);
-- 方式二:创建表时单独指定主键列
create table t_test_pk1(
id bigint auto_increment,
name varchar(20),
primary key(id)
);
-- 方式三:修改表中的列为主键索引
create table t_test_pk2(
id bigint,
name varchar(20)
);
alter table t_test_pk2 add primary key(id);
alter table t_test_pk2 modify id bigint auto_increment;
5.2 普通索引
-
最基本的索引类型,没有唯一性的限制。
-
可以为多个列创建组合索引,称为复合索引。
-
为了提升查询效率,可以为查询频繁的列创建普通索引。
普通索引的使用场景:
- 创建表的时候,明确知道某些列是频繁查询的列,就直接创建。
- 在工作中,一般表中数据量很大的时候,可以创建普通索引来提升查询效率。
- 若数据量非常小,有时全表扫描的效率或许会比创建普通索引效率还要高,因此需要在合适情况下使用。
创建普通索引:
sql
-- 创建表时指定索引列
create table t_test_index(
id bigint primary key auto_increment,
name varchar(20) unique,
sno varchar(20),
index(sno)
);
-- 修改表中的列为普通索引
create table t_test_index1(
id bigint primary key auto_increment,
name varchar(20),
sno varchar(20)
);
alter table t_test_index1 add index(sno);
-- 单独创建索引并指定索引名
create table t_test_index2(
id bigint primary key auto_increment,
name varchar(20),
sno varchar(20)
);
create index index_name on t_test_index2(sno);
5.3 唯一索引
- 在一个表上定义一个唯一键
unique
时,自动创建唯一索引。 - 与普通索引类似,但区别在于唯一索引的列不允许有重复值。
创建唯一索引:
sql
-- 创建表时创建唯一索引
create table t_test_uk(
id bigint primary key auto_increment,
name varchar(20) unique
);
-- 创建表时单独指定
create table t_test_uk1(
id bigint primary key auto_increment,
name varchar(20),
unique(name)
);
-- 修改表中的列为唯一索引
create table t_test_uk2(
id bigint primary key auto_increment,
name varchar(20)
);
alter table t_test_uk2 add unique(name);
5.4 全文索引
- 基于文本列(
CHAR
、VARCHAR
、TEXT
)上创建,以加快对这些列中包含的数据查询和DML操作。可以用于全文搜索,仅MyISAM
和InnoDB
支持。
5.5 聚集索引
- 与主键索引是同义词。
- 如果没有为表定义主键,
InnoDB
使用第一个unique
和not null
的列作为聚集索引。 - 如果表中没有主键或者合适的
unique
索引,InnoDB
会为新插入的行生成一个行号并且用6字节的ROW_ID
字段记录,ROW-ID
单调递增,并使用ROW-ID
作为索引。
5.6 非聚集索引
- 聚集索引以外的索引称为非聚集索引或二级索引。
- 非聚集索引中的每一条数据都包含该行的主键值,以及非聚集索引指定的列。
InnoDB
使用这个主键值来搜索聚集索引中的行,这个过程称为回表查询。
详细解释回表查询:
- 回表查询是MySQL中的一种查询方式,主要在使用二级索引(非聚簇索引)时发生。具体过程如下:
- 二级索引查找:首先通过二级索引找到对应的主键值。
- 回表操作:根据主键值回到聚簇索引中查找完整的行数据。
示例
假设有一张表users
,结构如下:
sql
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(100),
age INT,
INDEX idx_age (age)
);
执行查询:
sql
SELECT * FROM users WHERE age = 25;
- 使用二级索引:先在
idx_age
索引中找到age = 25
的主键值。 - 回表:根据主键值回到聚簇索引中获取完整的行数据。
性能影响
- 优点:二级索引占用空间小,查找速度快。
- 缺点:回表操作增加额外I/O,可能影响性能。
优化方法:
- 覆盖索引:确保查询字段都在索引中,避免回表。
- 索引优化:合理设计索引,减少回表次数。
5.7 索引覆盖
- 当需要查询的列包含在创建的索引列中,可以直接从普通索引返回,这个现象叫做索引覆盖。
举例:
sql
SELECT age FROM users WHERE age = 25;
上述代码会发生索引覆盖,这样的查询方式能够减少内存和磁盘IO,提升查找效率。
6. 查看索引
sql
show index from t_test_index;
7. 删除索引
删除具有自增属性的主键
sql
-- 直接删除含有自增属性的主键索引的列,会报错
alter table t_test_index drop primary key;
-- 错误信息 1075 - Incorrect table definition; there can be only one auto column and it must be defined as a key
-- 先删除自增属性
alter table t_test_index modify id bigint;
删除其他索引
sql
-- 直接删除其他索引
alter table t_test_uk1 drop index name;
-- name为表中索引名称
8. 总结
创建索引的注意事项:
- 索引应该创建在高频查询的列上。
- 索引需要占用额外的存储空间 -- 每一个索引都会生成对应的一个索引树。
- 对表进行插入、更新和删除操作的时候,同时也会修改索引,可能会影响性能 -- 因为要维护好一个索引树。
- 创建过多或不合理的索引会导致性能下降,需要谨慎选择和规划索引。