1.索引简介
MySQL的索引是⼀种数据结构 ,它可以帮助数据库高效地查询、更新数据表中的数据。索引通过 ⼀定的规则排列数据表中的记录,使得对表的查询可以通过对索引的搜索来加快速度。
MySQL 索引类似于书籍的目录,通过指向数据行的位置,可以快速定位和访问表中的数据,比如 汉语字典的目录(索引)页,我们可以按笔画、偏旁部首、拼音等排序的目录(索引)快速查找到需要的字。
使用索引 的目的只有⼀个,就是提升数据检索的效率(MySQL实现的两个关键目标安全和效率),在应⽤程序的运行过程中,查询操作的频率远远高于增删改的频率。
那么索引应该选择使用哪种数据结构呢?
- HASH:时间复杂度是O(1),查询的速度非常快,但是MySQL并没有选择HASH做为索引的默认数据结构,主要原因是HASH不支持范围查找。
- 二叉搜索树:二叉搜索树的中序遍历是⼀个有序数组,++支持范围查找++ ,但有几个问题导致它不适合用作索引的数据结构:
- 最坏情况下时间复杂度是O(n):二叉搜索树可能退化成一棵单边树。
- 节点个数过多无法保证树高:数据库中的数据是在磁盘上保存的,在检索数据时,每次访问某个节点的子节点时都会发生⼀次磁盘IO,而磁盘IO是制约数据库的主要因素。
- N叉树:为了解决树高的问题,可以使用N叉树 ------ 每个节点可以有超过两个的子节点 。时间复杂度是O(logn),也就意味着,在相同数据量的情况下,可以有效控制树高,减少IO的次数找到目标节点,从而提升效率。但是MySQL认为N叉树做为索引的数据结构还不够好。

2.B+树
B+树是⼀种经常用于数据库和文件系统等场合的平衡查找树,MySQL索引采用的数据结构。
以4阶 B+树为例,如下图所示:(阶:也叫度,每个节点最多有多少个子节点,一般子节点个数小于度的值)

- B+ 树(B+ Tree),它是一种有序的多路搜索树,满足 "左边节点值 < 右边节点值" 的有序性。是一种特殊的有序 N 叉树。
- B+ 树 的核心设计规则:
- 节点内有序 :每个节点里的关键字都按从小到大排列(比如根节点 0080, 0140 是递增的)。
- 子树范围有序 :
- 根节点 0080 指向的子树,所有值都小于 0080;
- 0080 和 0140 之间的子树,所有值都在 (0080, 0140) 之间;
- 0140 指向的子树,所有值都 大于 0140。
- 叶子节点有序且链表相连(双向链表) :最底层的叶子节点 0010,0020,0030...0200 是完整的有序链表,保证了全局有序。
- 只有叶子节点会被连成有序链表,这是为了遍历和范围查询。
- 非叶子节点(中间层、根节点) 不需要兄弟指针,它们只负责索引导航,数据都存在叶子节点里。
2.1 B+ 树的特点
- 能够保持数据稳定有序,插⼊与修改有较稳定的时间复杂度。
- 非叶⼦节点仅具有索引作⽤,不存储数据,所有叶子节点保真实数据。
- 所有叶子节点构成⼀个有序链表,可以按照key排序的次序依次遍历全部数据。
2.2 B+ 树与 B 树的对比
- 叶⼦节点中的数据是连续的,且叶子节点相互链接,可通过叶子节点找到它相邻的兄弟节点,便于区间查找和搜索 (MySQL在组织叶子节点时使用的是双向链表)。
- 非叶子节点的值都包含在叶子节点中
- MySQL非叶子节点只保存了对子节点的引用,没有保存真实的数据,所有真实数据都保存在叶子节点中。
- 对于B+树而言,在相同树高的情况下,查找任⼀元素的时间复杂度(O(logn))都⼀样,性能均衡。
到这里,我们已经了解了MySQL索引底层所使用的数据结构------B+ 树。那么索引在整个数据检索的过程中是如何工作的,就要从MySQL的存储结构说起。
>>> MySQL 的数据最终落地在磁盘文件 中,不同存储引擎(InnoDB/MyISAM)结构差异很大,其中 InnoDB 是默认且最核心的引擎 ,而InnoDB中的核心文件结构之一是 表空间文件(.ibd),它的作用是存储表数据 + 索引,一张数据表对应一个 .ibd 文件(默认),所有数据都在 B+ 树里。
.idb 是表空间文件的后缀。

而在 .ibd 文件中最重要的结构体就是Page(页)。
3.MySQL中的页
3.1为什么使用页
- 在 .ibd 文件中最重要的结构体就是Page(页)。
- 页是内存与磁盘交互的最小单元,默认大小为 16KB 。
- 可以理解为 B+ 树的节点就是一个默认大小16KB的页。
- B+ 树的叶子节点页:存储完整的行数据
- B+ 树的非叶子节点页:只存储索引键 + 指向子页的指针(导航用)
- 每次内存与磁盘的交互⾄少读取⼀页,所以在磁盘中每个页内部的地址都是连续的,之所以这样做,是因为在使⽤数据的过程中,根据局部性原理,将来要使用的数据大概率与当前访问的 数据在空间上是临近的,所以⼀次从磁盘中读取⼀页的数据放⼊内存中,当下次查询的数据还在这个页中时就可以从内存中直接读取,从而减少磁盘I/O提高性能。
- 每⼀个页中即使没有数据也会使用 16KB 的存储空间。查看页的大小,可以通过系统变量 innodb_page_size 查看

- 在MySQL中有多种不同类型的页,最常用的就是用来存储数据和索引 的"索引页",也叫做"数据 页"。但不论哪种类型的页都会包含页头(File Header)和页尾(File Trailer) ,页的主体信息使用数 据"行"进行填充,数据页的基本结构如下图所示:

前面说过一张表对应一个 .ibd文件,而一个 .idb文件中包含许多页 ,也就是说,一个 .ibd文件包含许多B+树。即一个 .ibd 文件 = 一大堆 16KB 页 = 一大堆 B+ 树节点

.ibd 里有大量数据页的根本原因:
是单页(16KB)存储容量有限 ,而一张表的实际数据量几乎都会超过这个上限,因此一页无法容纳整张表的所有数据,数据越多,数据页就越多。还有就是++B+ 树的 "分裂机制" 强制生成新页++ :

还有就是小页结构能最大化磁盘 I/O 和内存缓存的效率。
了解完页之后,我们继续回到数据页(叶子节点页)的基本结构,了解页头,页尾,页主体,页目录以及数据页头。
3.2页文件头和页文件尾
页文件头和页文件尾中包含的信息,如下图所示:

这里我们只关注,上⼀页页号和下⼀页页号 ,通过这两个属性可以把页与页之间链接起来 ,形成⼀个双向链表。(通过页号和页大小,可以计算出下一页和上一页在磁盘上的偏移量)
3.3页主体
页主体部分是保存真实数据的主要区域 ,每当创建⼀个新页,都会自动分配 两个行,一个是页内最 小行 Infimun ,另⼀个是页内最大行 Supremun ,这两个行并不存储任何真实信息 ,而是做为数据行链表的头和尾 ,第⼀个数据行有⼀个记录下⼀行的地址偏移量的区域 next_record 将页内所有数据行组成了⼀个单向链表,此时新页的结构如下所示:

当向⼀个新页插⼊数据时,将 Infimun 连接第⼀个数据行,最后⼀行真实数据行连接 Supremun ,这样数据行就构建成了⼀个单向链表 ,更多的行数据插入后,会按照主键从小到大的顺序进行链接,如下图所示:

事实上,最小行和最大行在未插入数据行时,是挨在一起的,当有数据插入时,都会插入在它们的中间,即页一初始化,就先把这两条写死,让它们挨在一起,形成一个空链表框架,当有数据行时,就往中间插入。
注意:最小行(Infimum)和最大行(Supremum)在物理存储位置 上依然是紧挨着页头的(Page Header 之后)(如下图是最大行和最小行真正的位置),只是在逻辑链表关系上,它们中间被插入了用户记录(如上面所画的图)。

总结:数据页初始化时,最小行与最大行会物理上固定相邻,形成空链表。无数据时二者直接相连;插入用户记录时,记录会被插入到二者的逻辑链表之间,但最小行和最大行的物理位置始终保持相邻不变。
3.4页目录
- 当按主键或索引查找某条数据时,最直接简单的方法就是从头行 infimun 开始 ,沿着链表顺序逐个比对查找 ,但⼀个页有16KB,通常会存在数百行数据,每次都要遍历数百行,无法满足高效查 询,为了提高查询效率,InnoDB采用**⼆分查找**来解决查询效率问题;
- 具体实现方式是,在每⼀个页中加入⼀个叫做页目录 Page Directory 的结构,将页内包括头行、尾行在内的所有行进行分组 ,约定头行单独为⼀组,其他每个组最多8条数据,++同时把每个组最后⼀行在页中的地址,按主键从小到大的顺序记录在页目录中++ ,页目录中的每⼀个位置称为⼀ 个槽 ,每个槽都对应了⼀个分组,⼀旦分组中的数据行超过分组的上限8个时,就会分裂出⼀个新的分组;
- 后续在查询某行时,就可以通过⼆分查找 ,先找到对应的槽,然后在槽内最多8个数据行中进行遍 历即可,从而大幅提高了查询效率,这时⼀个页的核心结构就完成了;
- 例如要查找主键为6的行,先比对槽中记录的主键值,定位到最后⼀个槽2,再从最后⼀个槽中的第 ⼀条记录遍历,第⼆条记录就是我们要查询的目标行。

3.5数据页头
数据页头记录了当前页保存数据相关的信息,如下图所示:

4.B+树在MySQL索引中的应用
- 非叶子节点保存索引数据(索引页,保存的是主键值和子节点的引用/指针),叶子节点保存真实数据(数据页,保存的是具体数据,页与页之间通过页号建立关联关系,叶子节点最终形成一个双向循环链表),如下图所示:

- 以查找id为5的记录,完整的检索过程如下:
- 首先判断B+树的根节点中的索引记录,此时 5 < 7 ,应访问左孩子节点,找到索引页2(所有关于页的访问都是在内存中进行的)
- 在索引页2中判断id的大小,找到与5相等的记录,命中,加载对应的数据页
- 以上的IO过程,加载索引页1 --> 加载索引页2 --> 加载数据页3
4.1计算三层树高的B+树可以存放多少条记录
- 假设⼀条用户数据大小为1KB,在忽略数据页中数据页 自身属性空间占用的情况下,⼀页可以存16 条数据。
- 索引页 ⼀条数据的大小为:主键用BIGINT类型占 8 Byte,下⼀页地址 6 Byte,⼀共是14 Byte,⼀个索引页可以保存16*1024/14 = 1170条索引记录。
- 如果只有三层树高的情况,综合只保存索引的根节点和⼆级节点的索引页以及保存真实数据的数据 页,那么⼀共可以保存1170*1170*16 = 21,902,400 条记录,也就是说在两千多万条数据的表中,可以通过三次IO就完成数据的检索。
5.索引分类
5.1主键索引
- 当在⼀个表上定义⼀个主键 PRIMARY KEY 时,InnoDB使用它作为聚集索引(聚簇索引)。
- 推荐为每个表定义⼀个主键。如果没有逻辑上唯⼀且非空的列或列集可以使用主键,则添加⼀个⾃ 增列。
- 一张表只能有一个聚集 / 聚簇索引,也就是一张表只能有一个主键列。
5.2普通索引
- 最基本的索引类型,没有唯⼀性的限制。
- 可能为++多列创建组合索引++ ,称为复合索引 或组全索引。
5.3唯一索引
- 当在⼀个表上定义⼀个唯⼀键 UNQUE 时,自动创建唯⼀索引。
- 与普通索引类似,但区别在于唯⼀索引的列不允许有重复值。
5.4全文索引
- 基于文本列(CHAR、VARCHAR或TEXT列)上创建,以加快对这些列中包含的数据查询和DML操作。
- 用于全文搜索,仅MyISAM和InnoDB引擎支持。
5.5聚集索引
- 与主键索引是同义词。
- 如果没有为表定义 PRIMARY KEY, InnoDB使用第⼀个 UNIQUE 和 NOT NULL 的列作为聚集索引。
- 如果表中没有 PRIMARY KEY 或合适的 UNIQUE 索引,InnoDB会为新插入的行生成⼀个行号并 用6字节的 ROW_ID 字段(数据行中的隐藏列/隐式字段) 记录, ROW_ID 单调递增,并使用 ROW_ID 做为索引。
聚集索引和主键索引的区别
先抛出核心结论:主键索引 ≠ 聚集索引,但在 InnoDB 里,主键索引默认就是聚集索引(是 "默认绑定",而非 "天生等同")。
两个概念的本质:

InnoDB 引擎规定:如果表定义了主键,那么这个主键索引会被自动设为聚集索引。

在日常情况下,主键索引几乎就是聚集索引,只有一种情况能看出 主键索引≠聚集索引 区别:表没有显式主键时,InnoDB 会选其他字段做聚集索引,但这个字段不是主键。
示例:
sql
create table user (
id int auto_increment,
username varchar(50) not null unique,
age int
);

一个形象的例子:

总结:
- 本质区别:主键索引是 "字段维度" 的索引(建在主键上),聚集索引是 "存储维度" 的索引(数据与索引物理绑定);
- 默认关联:InnoDB 中,主键索引默认就是聚集索引(这是最常见的情况);
- 特殊情况:无显式主键时,InnoDB 会选唯一非空索引 / 隐式 row_id 作为聚集索引,此时聚集索引≠主键索引。
5.6非聚集索引
- 聚集索引以外的索引称为⾮聚集索引或⼆级索引:也就是说 主键索引 = 聚集索引 ;普通索引,唯一索引等都是非聚集索引。
- 聚集索引:主键专属,存整行数据,即叶子节点存储完整数据
- 非聚集索引:其他所有索引,只存索引 + 主键,即叶子节点不存完整数据,查询时需要回表 。
- 回表查询:当你用「非聚集索引」(普通 / 唯一 / 复合索引)查询数据时,因为非聚集索引的叶子节点只存「索引值 + 主键值」,没有完整数据,所以需要拿着主键值再去「聚集索引」里查完整数据 ------ 这个 "二次查询" 的过程,就是回表。
- 简单说:回表 = 「非聚集索引查主键」 + 「聚集索引查完整数据」。

它们之间的引用关系:非聚集索引树的叶子节点 → 存主键 → 指向聚集索引树。
我们可以再进行一个更加完善的总结:
一张表对应一个 .ibd 文件 → .ibd 文件由大量 16KB 的页(数据页、索引页等)组成 → 这些页按照所属的索引被划分为多个组,每组页分别属于一棵逻辑上独立的 B+ 树(1 棵聚集索引树 + N 棵非聚集索引树),即每棵树独占一部分页,页与页之间不混用 → 最终:一个 .ibd 文件 = 众多 16KB 页的集合,是存储了 1+N 棵独立 B+ 树的物理容器。
回到非聚集索引,我们知道在一棵非聚集索引树中无法查询到完整的数据,需要回表查询到聚集索引树中查询数据信息,但是,如果使用索引覆盖,那么就可以直接在非聚集索引树中获取数据,而无需回表。
5.7索引覆盖
索引覆盖 是指:查询语句中需要获取的所有字段(SELECT 列 + WHERE 条件列),都能从某一棵非聚集索引的 B+ 树中直接获取,无需回表查询聚集索引。
简单说:索引 "覆盖" 了查询的所有需求,不用再去查数据本体(聚集索引)。
核心判定条件:
- 查询的所有字段(如 id、age、name)都包含在目标非聚集索引中;
- 查询条件字段是该索引的列(或复合索引的最左前缀);
- 全程仅操作非聚集索引的 B+ 树,不依赖聚集索引。
示例:用户表:主键 id,非聚集索引 idx_age(age)
sql
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT, -- 聚集索引:叶子节点存 id+name+age+...(完整数据)
name VARCHAR(50),
age INT,
INDEX idx_age (age) -- 非聚集索引:叶子节点只存 age + id
);
sql
-- 普通查询(回表,2 次 IO)
SELECT id, name FROM user WHERE age = 20;
-- 索引覆盖查询(不回表,1 次 IO)
SELECT id, age FROM user WHERE age = 20;
记住回表查询的核心流程是:先遍历非聚集索引树拿主键 → 再遍历聚集索引树补全数据。那么普通索引的回表流程:

而索引覆盖的例子:

分析:虽然聚集索引也有主键值,但是你要查的是 "age=20",不是 "id=5"------ 聚集索引里没有按 age 排序的结构,无法快速找到 age=20 的行,必须靠非聚集索引先找到对应的 id:非聚集索引(如 idx_age)是按条件字段(age)排序的,能快速找到目标数据对应的聚合索引的主键从而找到 name。
回表必要流程:
查age=20 → 非聚集索引(按age排序)→ 拿ID5 → 聚集索引(按ID排序)→ 拿name
到这里,我们对索引的了解全部结束,接下来我们要来使用索引。
6.使用索引
6.1自动创建
- 当我们为⼀张表加主键约束(Primary key),外键约束(Foreign Key),唯⼀约束(Unique)时, MySQL会为对应的的列自动创建⼀个索引。
- 如果表不指定任何约束时,MySQL会自动为每⼀列生成⼀个索引并用 ROW_ID 进行标识。
6.2手动创建
6.2.1主键索引
- 方式一:创建表时创建主键
sql
create table t_pk1 (
id bigint primary key auto_increment,
name varchar(20)
);
如上述的例子,我们在创建表时就加了一个主键约束,即此时这个表的主键列对应有一个主键索引,那么可以通过show index from 表名; 这条SQL语句查看指定表的索引信息:

- Table:索引所属的表名。
- Non_unique:是否唯一索引(0 表示唯一,1 表示可以重复)。
- Key_name:索引名称(主键索引默认为 PRIMARY)。
- Seq_in_index:索引中列的顺序/序号(从 1 开始)。复合索引中用于标识列的位置(复合主键列谁先谁后)。
- Column_name:索引的列名。
- Collation:列的排序方式(A 表示升序,NULL 表示不排序)。
- Cardinality:索引中唯一值的估计数量,用于优化器判断索引效率。
- Sub_part:如果只索引列的前缀部分,显示前缀长度(如对 varchar(255) 只索引前 10 个字符,则显示 10)。
- Packed:索引是否被压缩(NULL 表示未压缩)。
- Null:列是否允许 NULL 值(YES 表示允许)。
- Index_type:索引类型(BTREE、HASH、FULLTEXT 等,InnoDB 通常为 BTREE)。
- Comment:索引的注释信息。
- Index_comment:创建索引时指定的注释。
- 方式二:创建表时单独指定主键列
sql
create table t_pk2 (
id bigint auto_increment,
name varchar(20),
primary key(id)
);
- 方式三:修改表中的列为主键索引
sql
create table t_pk3 (
id bigint,
name varchar(20),
);
alter table t_pk3 add primary key(id); -- 为t_pk3的表添加主键并指定列
alter table t_pk3 modify id bigint auto_increment; -- 把t_pk3表中id列修改为自增列
6.2.2唯一索引
- 方式一:创建表时创建唯一键
sql
create table t_uq1 (
id bigint primary key auto_increment,
name varchar(20) unique
);
查看表索引信息:

- 方式二:创建表时单独指定唯一列
sql
create table t_uq2 (
id bigint primary key auto_increment,
name varchar(20),
unique (name)
);
- 方式三:修改表中的列为唯一索引
sql
create table t_uq3 (
id bigint primary key auto_increment,
name varchar(20),
);
alter table t_uq3 add unique (name);
6.2.3普通索引
普通索引创建的时机:
- 创建表的时候,明确知道某些列频繁查询,就创建好(当表中数据过少时,全表扫描可能效率比索引高)。
- 随着业务不断发展,在版本迭代过程中添加索引。
创建普通索引语法:
index 是创建索引关键字。
sql
index (索引列列名);
- 方式一:创建表时指定索引列
sql
create table t_index1 (
id bigint primary key auto_increment,
name varchar(20) unique
sno varchar(20),
index (sno)
);
查看索引信息:


- 方式二:修改表中的列为普通索引
sql
create table t_index2 (
id bigint primary key auto_increment,
name varchar(20) unique
sno varchar(20),
);
alter table t_index2 add index (sno);
- 方式三:单独创建索引并指定索引名(推荐使用)
语法:
sql
create index 索引名 on 表名(列名[,列名]);
sql
create table t_index3 (
id bigint primary key auto_increment,
name varchar(20) unique
sno varchar(20),
);
-- 为 t_index3 表创建普通索引,索引列为sno
create index sno_index3 on t_index3(sno);
查看索引信息:可以看到,当我们单独去创建一个索引时,可以指定索引名,而不是默认是将作为索引的列的列名作为索引名:

6.2.4复合索引
创建语法与创建普通索引相同,只不过指定多个列(索引中包含多个列),列与列之间⽤逗号隔开。
- 方式一:创建表时指定索引列
sql
create table t_index4 (
id bigint primary key auto_increment,
name varchar(20),
sno varchar(20),
class_id bigint,
index (sno,class_id)
);
查看索引信息:

- 方式二:修改表中的列为复合索引
sql
create table t_index5 (
id bigint primary key auto_increment,
name varchar(20),
sno varchar(20),
class_id bigint
);
alter table t_index5 add index (sno,class_id);
- 方式三:单独创建索引并指定索引名(推荐使用)
sql
create table t_index6 (
id bigint primary key auto_increment,
name varchar(20),
sno varchar(20),
class_id bigint
);
-- 为t_index6表创建复合索引并指定索引列
create index sno_class_id on t_index6(sno,class_id);
查看索引信息:

6.3查看索引
- 方式一:show keys from 表名;
示例:

- 方式二:show index from 表名;
示例:

方式三:desc 表名; ------ 查看的是简要信息
示例:

6.4删除索引
6.4.1删除主键索引
语法:
sql
alter table 表名 drop primary key;
示例:删除 t_index6 表中的主键

此时发现此条语句运行失败,原因是该表中的主键列设置为了自增列,需要先删除自增属性,然后再删除主键:


查看索引信息:主键索引已经被删除,只剩下一个复合索引

6.4.2删除其他索引
这里的其他索引指的是除了主键索引之外的索引,例如唯一索引,复合索引,普通索引等。
语法:
sql
alter table 表名 drop index 索引名;
示例:删除 t_index6 表中名为 sno_class_id 的索引

查看索引信息:此时的表中已经没有索引,是空表


7.如何查看自己写的SQL走没走索引
可以看执行计划:explain
示例:有一个stu表

先为这个表创建一个复合索引:

查看索引信息:

查看表中的数据:

- 1.查看执行计划:以下是不加条件,即查询所有

- 2.使用主键查询:

- 3.子查询中使用索引

- 4.唯一索引查询

- 5.复合/普通索引查询

以上的查询语句都是需要回表查询的(主键索引除外),除了在子查询的时候使用了索引覆盖。再举一个索引覆盖的例子:用复合索引 sno_class_id 举例:

- Extra------执行情况的说明和描述。
- Using index:表示使用索引,如果只有Using index ,说明没有查询到数据表,只用到了索引表就完成了这个查询,这个叫做索引覆盖。
- Using where:表示条件查询,如果不读取表的所有数据,或不是仅仅通过索引就可以获取所有需要的数据,则会出现Using where。
- Using where; Using index:表示查询使用了覆盖索引,但索引返回的行还需要经过 WHERE 条件进一步过滤(虽然列都在索引中,但索引本身可能无法直接排除所有行,仍需在索引层面筛选)
总结:只要查询条件中使用了索引包含的索引列,就会走索引,和顺序无关。
8.创建索引的注意事项
- 索引应该创建在高频查询的列上
- 索引需要占用额外的存储空间
- 对表进行插入、更新和删除操作时,同时也会修索引,可能会影响性能
- 创建过多或不合理的索引会导致性能下降,需要谨慎选择和规划索引