MySQL数据库 (十二) MySQL索引(上),认识索引,认识Page,B+树索引结构,聚簇索引和非聚簇索引

目录

一、索引

认识索引

索引的分类

示例:

二、了解硬件

MySQL与存储

认识磁盘

[MySQL 与磁盘交互基本单位---理解软件](#MySQL 与磁盘交互基本单位---理解软件)

四、索引的理解

重谈page

理解单个page

理解多个page

B+树索引结构

五、聚簇索引和非聚簇索引

[MyISAM 存储引擎](#MyISAM 存储引擎)

六、总结


我们现在正式进入 MySQL 索引的学习。

一、索引

认识索引

首先要理解索引的核心作用:它的本质就是为了提高数据库的性能,尤其是查询性能。我们可以把索引理解成书籍的目录,有了目录,我们就不用逐页翻找内容,而是可以直接定位到目标位置,这和数据库中索引加速查询的逻辑是完全一样的。

MySQL 服务器的所有数据操作,本质上都是在内存中完成的,索引也不例外。而决定算法效率的关键因素有两个:**一是数据的组织方式,二是算法本身。**索引正是通过改变数据的组织方式,也就是重构数据结构来优化查询效率的,这就是我们常说的 "结构决定算法"。它的好处非常明显,不需要修改程序,也不用调整 SQL 语句,只要通过 create index 创建合适的索引,查询速度就可能提升成百上千倍,是数据库优化中性价比极高的手段。但也要注意,索引并非没有代价,天下没有免费的午餐,查询性能的提升,是以插入、更新、删除等写操作的速度为代价的,因为维护索引需要额外的 IO 开销。所以,索引的核心价值就是在需要频繁查询的场景下,用写操作的少量性能损耗,换取海量数据检索时的巨大效率提升。

索引的分类

接下来我们看索引的常见分类,这些也是实际开发中最常用的几种索引类型。

  1. 主键索引,也就是通过 primary key 定义的索引,它不仅能加速查询,还自带唯一性约束,一张表只能有一个主键索引,且字段值不能为 NULL;
  2. 唯一索引 unique,和主键索引类似,也能保证字段值的唯一性,但它允许字段为 NULL,一张表可以创建多个唯一索引;
  3. 普通索引 index,是最基础的索引类型,仅用于加速查询,不具备唯一性约束,主要用于提升非唯一字段的查询效率;
  4. 全文索引 fulltext,则是专门为了解决文本内容的检索问题,比如在大段的文章中快速匹配关键词,在需要进行模糊文本搜索的场景中非常实用。

示例:

我们用一个 800 万条数据的实际例子来直观感受索引的作用。为了模拟真实的海量数据场景,我们首先通过存储过程和自定义函数,批量生成了 800 万条随机的员工数据,这些数据包含随机生成的姓名、部门、薪资等信息,确保了数据的差异性,这样才能真实反映出不同查询方式的效率差异。如下:

bash 复制代码
--构建一个8000000条记录的数据
--构建的海量表数据需要有差异性,所以使用存储过程来创建, 拷贝下面代码就可以了,暂时不用理解
-- 产生随机字符串
delimiter $$
create function rand_string(n INT)
returns varchar(255)
begin
declare chars_str varchar(100) default
'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
declare return_str varchar(255) default '';
declare i int default 0;
while i < n do
set return_str =concat(return_str,substring(chars_str,floor(1+rand()*52),1));
set i = i + 1;
end while;
return return_str;
end $$
delimiter ;

--产生随机数字
delimiter $$
create function rand_num()
returns int(5)
begin
declare i int default 0;
set i = floor(10+rand()*500);
return i;
end $$
delimiter ;

--创建存储过程,向雇员表添加海量数据
delimiter $$
create procedure insert_emp(in start int(10),in max_num int(10))
begin
declare i int default 0;
set autocommit = 0;
repeat
set i = i + 1;
insert into EMP values ((start+i)
,rand_string(6),'SALESMAN',0001,curdate(),2000,400,rand_num());
until i = max_num
end repeat;
commit;
end $$
delimiter ;

-- 执行存储过程,添加8000000条记录
call insert_emp(100001, 8000000);

最终表结构中就有8000000条员工记录,也就是创建出了海量的数据。

创建完成后,我们通过 select * from EMP limit 5; 查看了前 5 条数据,可以看到这些记录都是随机生成的,表中共有 800 万条数据,直接全表查询会非常耗时,所以只能用 limit 来查看部分内容。
接下来我们开始实验,在没有任何索引的情况下,查询员工编号为 998877 的员工,


执行语句 select * from EMP where empno=998877;,可以看到查询耗时达到了 6.17 秒。 这是因为此时数据库采用的是全表扫描(线性遍历) ,也就是从表的第一条记录开始,一条一条地往后比对,直到找到匹配的员工编号为止。在本机单用户操作下就需要 6 秒多,如果是实际项目中多人并发查询,很容易造成数据库压力过大,甚至出现系统卡顿的情况,这也体现了无索引时海量数据查询的低效问题。
那解决方法是什么? 创建索引

我们为员工编号字段创建索引,执行语句 alter table EMP add index(empno); ,创建索引本身也需要一定的时间,这里耗时 25.29 秒,因为数据库需要为这 800 万条数据构建索引结构。
此时再查询998877员工编号,测试看看查询时间:

索引创建完成后,我们再次执行相同的查询语句,查询员工编号为 998877 的员工,这次的查询耗时直接降到了 0.00 秒,几乎是瞬间完成。这是因为索引改变了数据的组织方式,数据库不再需要逐条遍历,而是可以通过索引结构直接定位到目标记录,查询效率得到了质的提升。

通过这两次查询的对比,我们可以非常直观地看到索引的核心优势:它以创建索引时的少量时间开销,以及后续写操作(插入、更新、删除)的额外 IO 开销为代价,换来了海量数据查询时效率的巨大提升,这也是索引在数据库优化中被广泛使用的根本原因。

下面我们就逐步学习索引,我们先来了解一下硬件。

二、了解硬件

索引的设计和硬件特性其实是深度绑定的,我们前面看到的 "索引能大幅提升查询效率",背后的核心原因,就是它充分利用了磁盘的硬件特性来减少不必要的 IO 操作,所以我们需要先了解磁盘的工作原理,才能真正理解索引为什么能这么高效。

MySQL与存储

MySQL 给用户提供存储服务,而所有数据最终都存放在磁盘这个外部设备中。和内存相比,磁盘是机械部件,读写效率本身就低很多,再加上磁盘 IO 的物理特性,如何减少 IO 次数、提升数据读取效率,就成了 MySQL 性能优化的核心话题,而索引正是为了应对磁盘的这些特性而设计的。

认识磁盘

先来研究一下磁盘:

我们先来看磁盘的整体构造,从图中可以看到,磁盘主要由盘片、磁头、磁头臂、主轴马达等部件组成。盘片是存储数据的载体,主轴马达带动盘片高速旋转,磁头臂则带着磁头在盘片上方移动,通过改变位置来读写不同位置的数据。磁头平时停放在磁头停泊区,只有读写数据时才会移动到对应的磁道上,这种机械移动的过程,就是磁盘 IO 耗时的主要来源之一。
在看看磁盘中一个盘片:

再看盘片的内部结构,盘片表面被划分成许多同心圆,每个同心圆就叫做一个磁道,每个磁道又被进一步划分为多个扇区。扇区是磁盘的最小存储单元,传统扇区大小为 512 字节,现在很多磁盘已经升级为 4096 字节的 4K 扇区。数据就存储在这些扇区里,磁头移动到目标磁道后,会等待盘片旋转到对应的扇区位置,才能完成数据读写,这就带来了两个关键的耗时因素:磁头移动的 "寻道时间" 和盘片旋转的 "旋转延迟"。

索引的核心作用,就是通过改变数据的组织方式,让 MySQL 在查询时,只需要读取少量的磁盘扇区,就能找到目标数据,而不用像全表扫描那样,逐个读取所有扇区,大幅减少了机械移动和 IO 次数,从而利用磁盘的硬件特性,实现查询效率的提升。
扇区:
我们接着前面的磁盘硬件话题,来看更底层的扇区相关知识:


数据库文件本质上就是存放在磁盘盘片上的,而盘片上划分的一个个 "小格子",就是我们常说的扇区。 数据库文件通常很大,一个文件会占据磁盘上的多个扇区,所以要读取完整的数据库文件,本质上就是找到并读取所有存储了该文件数据的扇区。磁盘上的扇区默认大小是 512 字节,现在也有 4096 字节的 4K 扇区,但我们这里先以传统的 512 字节扇区为例理解,而且从结构上看,盘片上的扇区并不是一样大的,距离圆心越近,扇区越小;距离圆心越远,扇区越大,这是由磁盘的比特位密度决定的。
在 Linux 系统中,我们看到的大部分文件,也都是存储在硬盘的扇区里的,而定位一个文件的所有数据,本质上就是定位它占用的所有扇区,只要能定位到一个扇区,就能通过相同的方式找到同文件的其他扇区。
定位扇区:

接下来我们看扇区的定位方式。要在磁盘上找到一个具体的扇区,需要通过三个关键信息: 磁头、柱面和扇区编号 。多个盘片同半径的磁道会共同构成一个柱面,每个盘面对应一个磁头,磁头、柱面和扇区编号组合起来,就能唯一确定磁盘上的一个扇区,这种定位方式叫做 CHS 寻址。不过实际系统软件中使用的是 LBA 线性地址,系统会把逻辑地址转化为 CHS 地址交给磁盘,我们不用关心转化细节,只要知道这种寻址方式能让我们精准定位到任何一个扇区即可。
结论:
但系统并不会直接以扇区为单位和磁盘交互,这里我们可以得出几个关键结论。

  1. 首先,如果操作系统直接用扇区大小作为 IO 单位,系统代码就会和硬件强绑定,硬件规格变化时系统也要跟着修改,非常不灵活;
  2. 其次,单次 IO 只读取 512 字节效率太低,读取大量数据时需要多次磁盘访问,会大幅降低效率。
  3. 所以文件系统会定义更大的 IO 单位,也就是数据块,通常是 4KB,相当于 8 个传统扇区,系统会以块为单位进行磁盘读写,这样既能减少 IO 次数,也能让系统和硬件解耦。

磁盘随机访问 (Random Access) 与连续访问 (Sequential Access)
最后我们来看磁盘的随机访问和连续访问。 随机访问是指两次 IO 操作的扇区地址不连续,磁头需要在两次操作之间移动位置,寻道时间长,效率很低 ; 而连续访问是指两次 IO 的扇区地址是连续的,磁头不需要大幅移动,能快速完成读写操作,效率很高 。索引的设计,本质上就是利用了磁盘的连续访问特性,通过有序组织数据,让数据库在查询时尽可能进行连续 IO,减少随机访问的次数,从而大幅提升查询效率(局部性原理)。

MySQL 与磁盘交互基本单位---理解软件

我们接着前面的磁盘硬件话题,来看 MySQL 与磁盘交互的软件层面逻辑,重点理解 MySQL 和操作系统、磁盘之间的数据流转过程。


首先,我们可以把 MySQL 理解成运行在操作系统之上的一款特殊文件系统,MySQL 的核心任务就是管理磁盘上的数据。从架构上看,它分为三个层级:最上层是应用层的 MySQL 进程,中间是操作系统 OS,最底层是磁盘硬件。MySQL 进程无法直接和磁盘通信,所有数据读写都必须通过操作系统来完成,这是现代操作系统的基本机制,所有应用程序都不能直接操作硬件,必须通过系统调用完成。
我们先看数据写入的路径。当 MySQL 需要把数据写入磁盘时,它不会直接和磁盘交互,而是先把数据写入自己的缓冲区 ------Buffer Pool。 Buffer Pool 是 MySQL 专门为了提高 IO 效率设计的内存缓冲区,数据在这里被组织成 MySQL 的基本交互单位,也就是 16KB 大小的 "页" (Page)。 随后,MySQL 通过文件描述符 fd,向操作系统发起 write 系统调用,把 16KB 的数据写入操作系统的文件缓存区。文件描述符 fd 在这里的作用,就像一个 "文件的钥匙",MySQL 通过它来指定要操作的磁盘文件,操作系统通过它来识别对应的文件对象,完成后续的数据转发。
接下来,操作系统和磁盘之间的数据交互,是以系统默认的 4KB 为单位进行的。MySQL 传入的 16KB 数据,会被操作系统拆分成 4 个 4KB 的数据块,分批写入磁盘。为了确保数据真正落盘,而不是停留在操作系统缓存中,MySQL 会调用 fsync 系统调用,强制操作系统把缓存中的数据同步刷新到磁盘硬件上,确保数据持久化。这个过程中,操作系统起到了承上启下的作用,它接收上层应用的 16KB 数据,再按照磁盘和系统的 4KB 单位进行拆分和写入,最终把数据存到磁盘的扇区里。

最后,我们可以通过 show global status like 'innodb_page_size'; 查看 MySQL 的交互单位,结果显示为 16384 字节,换算后正好是 16KB。这说明,虽然磁盘的基本单位是 512 字节,操作系统的 IO 单位是 4KB,但 MySQL 为了平衡 IO 效率和数据管理的灵活性,把自己和磁盘交互的基本单位设置为了 16KB,这个单位在 MySQL 中就叫做 "页"。每一次 MySQL 和磁盘的 IO 操作,都是以 16KB 的页为单位进行的,这也是后续 B + 树索引结构设计的基础。


我们接着前面的内容,深入理解 MySQL 中 "页(Page)" 和缓冲池(Buffer Pool)的核心逻辑:

MySQL 的数据文件本质上都是以 16KB 的页为单位存储在磁盘上的。所有的 CURD 操作,无论是插入、查询还是修改数据,都需要先找到对应的页,再在页内完成后续操作。而 CPU 处理数据必须在内存中进行,所以 MySQL 会先把磁盘上的页加载到内存,操作完成后再按策略刷新回磁盘,这个过程就是磁盘与内存的数据交互,而交互的基本单位,正是这 16KB 的页。

所以为了高效管理这些内存中的数据,MySQL 在启动时就会申请一块专门的内存空间,也就是我们上面说的 Buffer Pool。Buffer Pool 本质上是一个大内存缓冲区,专门用来缓存从磁盘加载的页数据,所有的 IO 交互都通过它来完成。这样一来,频繁访问的页会被保留在内存中,不用每次都重新从磁盘读取,能大幅减少磁盘 IO 次数,这也是 MySQL 提升性能的关键手段之一。

Buffer Pool 的大小可以通过配置文件中的 innodb_buffer_pool_size参数来设置,默认值通常为 128M,我们可以根据服务器的内存情况进行调整,比如在专用的数据库服务器上,一般会设置为总内存的 70% 左右,让 MySQL 尽可能缓存更多的数据页,减少磁盘 IO 压力。

基于以上内容,我们可以总结出几个关键共识:

  1. MySQL 以 16KB 的页为单位进行 IO 交互;
  2. 所有数据读写都先经过 Buffer Pool,读取时加载到池里,刷新时再从池里同步到操作系统缓存,最终写入磁盘;
  3. 优化的核心目标,就是尽可能减少系统和磁盘的 IO 次数,让更多操作在内存中完成。

四、索引的理解

我们通过这个简单的建表和插入数据的例子,来直观理解索引的作用。

先建表:

首先,我们创建了一张 user 表,其中 id 字段被设置为 primary key 主键。从建表语句和表结构可以看到,id 字段带有 PRI 标识,这意味着 MySQL 会自动为 id 创建主键索引,这是我们接下来要观察的关键点。

再插入数据:

接着,我们向表中插入数据,但插入的顺序是完全乱序的:先插入 id 为 3 的数据,再插入 4、2、5,最后才插入 id 为 1 的数据。

现在,当我们执行 select * from user; 查询数据时,会发现返回的结果是按 id 从小到大有序排列的。这就引出了问题:明明我们是乱序插入的,为什么查询结果却是有序的?是谁帮我们排好序的?

答案就是 MySQL 为 id 创建的主键索引。因为主键索引的底层是 B + 树结构,为了保证查询效率,数据在物理存储上就是按索引键值有序组织的。即使我们乱序插入数据,MySQL 也会按照主键索引的规则,将数据有序地存放在对应的位置,所以我们查询时看到的自然就是有序的结果了。这也是索引改变数据组织方式的直接体现。

重谈page

我们继续深入理解 MySQL 中的 page 概念,这是理解索引和 IO 优化的关键。

首先,MySQL 内部会同时存在大量的page,所以它必须对这些 page 进行统一管理。这就要求我们不能简单地把 page 看作一个普通的内存块,它内部必须包含管理信息,用来记录和维护自身的状态。上图中的 struct page 结构体就是这种管理方式的模型,它包含了 next 和 prev 指针,用来把所有的 page 串联成一个双向链表,方便在 Buffer Pool 中进行管理;同时还有 buffer 数组,用来存储真正的数据。这个结构体就是 MySQL 在内存中对 page 的建模,既包含了管理元数据,也包含了数据存储区域。

接下来我们要理解,为什么 MySQL 和磁盘交互时,要采用以 page 为单位的批量读取,而不是 "用多少加载多少"?

我们可以用图片中的例子来理解:如果我们每次只读取一条记录,那么查询 id=2 需要 1 次 IO,查询 id=5 又需要 1 次 IO,数据越多,IO 次数就越多,效率极低。而如果我们把多条记录都存放在同一个 page 中(比如 16KB 的页可以存放很多行数据),那么第一次查询 id=2 时,整个 page 会被一次性加载到 Buffer Pool 中,完成 1 次 IO。后续查询 id=1,3,4,5时,数据已经在内存中了,完全不需要再进行磁盘 IO,直接在内存中读取即可,这就大幅减少了 IO 次数。

这里的核心逻辑,就是计算机科学中的局部性原理。它的意思是,当我们访问一条数据时,它周围的数据在很大概率上也会被很快访问到。所以,一次性把相邻的数据加载到内存中,就能避免后续频繁的磁盘 IO。而磁盘 IO 效率低下的主要矛盾,从来都不是单次数据量的大小,而是 IO 的次数。减少 IO 次数,才是提升性能的关键,这也是 MySQL 采用page作为交互单位的根本原因。

理解单个page

我们先来理解 MySQL 中的单个page,看看它内部的结构和数据组织方式。

MySQL 中要管理很多数据表文件,而要管理好这些文件,就需要先描述再组织,我们目前可以简单理解成个独立文件是有一个一个或者多个 Page 构成的。

首先,每个 page 都是一个独立的存储单元,大小固定为 16KB。为了让 MySQL 能高效管理大量的 page,每个 page 都包含了两个关键的指针:page_prev和page_next。这两个指针分别指向前一个和后一个 page,把所有的 page 串联成了一个双向链表,这样 MySQL 就能通过链表结构,快速遍历和管理所有的 page 了。

再看 page 内部的数据组织,从图中可以看到,里面的记录是按主键顺序有序排列的,比如 1、2、3、4、5 这样依次存储。即使我们之前是乱序插入数据,MySQL 也会按照主键的顺序,把数据有序地存放到 page 里,这也是我们之前乱序插入但查询结果有序的根本原因。page 内部的数据本质上也是一个链表结构,这种结构的特点是插入、删除很快,但顺序查找效率不高,所以 MySQL 才会在page内部把数据按主键排序,以此来优化查询效率。

为什么插入数据时要特意排序呢?核心目的就是优化查询效率。如果数据是有序的,那么在查找数据时,我们可以按顺序高效比对,不会做无效的查找;而且在某些情况下,还能提前结束查找过程,减少不必要的操作。这种有序的组织方式,让page内部的数据查询变得更高效,也是后续 B + 树索引能够快速定位数据的基础。

理解多个page

我们接着前面单个page的话题,来看多个page是如何组织的,以及为什么需要引入页目录的概念。

首先,当数据量很大时,比如上千万条记录,单个 page 是存不下的,这时就需要多个 page 来存储数据。从图中可以看到,这些 page 之间是通过双向链表连接起来的,每个 page 的 page_prev 指向前一个 page,page_next 指向后一个 page;而每个 page 内部的数据,也是通过链表结构有序存储的。这就带来了一个问题:无论是在 page 内部,还是在多个 page 之间,数据的查找本质上都是线性遍历,因为都是链表形式的,这就导致效率非常低。比如要找某条记录,我们需要先从第一个 page 开始,逐个遍历每个 page,再在 page 内逐条比对数据,这样的查找方式在海量数据场景下几乎不可用。

页目录:

为了解决这个问题,我们需要同时提高 page 内和 page 间的查找效率,就引入了页目录的概念。这就像我们看书时,要找 "指针" 这一章,从头翻页会很慢,但如果有了目录,我们就能直接根据目录里的页码快速定位到目标章节,而不用逐页查找。页目录就是 page 内部的 "目录",它会记录 page 内关键数据的位置信息,比如每个数据段的起始键值和对应的偏移地址。这样一来,查找数据时就不用逐条遍历,而是可以通过页目录快速缩小查找范围,直接定位到目标数据所在的位置。

页目录的本质,就是典型的 "空间换时间" 的做法。它需要占用额外的存储空间来记录目录信息,但换来了查找效率的大幅提升,这也是索引优化的核心思想之一。后续我们要学习的 B + 树索引,正是基于这种页目录的思路,把多个page组织成了更高效的树形结构,让数据查找从线性遍历变成了对数级的快速定位。

单页情况:

我们接着前面的内容,来看单个 page 内部是如何引入目录来提升查找效率的。

在单个 page 中,数据是按链表形式有序存储的。如果不引入目录,要查找 id=4 的记录,就必须从第一条记录开始,逐条遍历比对,直到找到目标数据,这里需要遍历 4 次才能拿到结果,效率很低。

现在,我们在 page 内部引入了页目录。从图中可以看到,目录里记录了关键数据的位置信息,比如目录11指向第一条数据,目录23指向第三条数据。这样一来,当我们查找 id=4 的记录时,就可以先通过目录快速定位到它所在的区间,再在区间内查找,不用再逐条遍历前面的记录,大幅减少了查找次数,效率得到了明显提升。这也正是 MySQL 会自动按主键排序的根本原因:只有数据有序,我们才能方便地引入页目录,让目录中的记录能按顺序划分区间,实现快速定位。如果数据是乱序的,目录就无法发挥作用,我们依然只能线性遍历。

页目录解决了单个 page 内部的查找效率问题,但多个 page 之间的查找效率问题还没有解决,这也是我们接下来要解决的问题。

多页情况:

我们接着前面单页目录的话题,来看多页情况下的组织方式,以及如何解决多页之间的查找效率问题。

MySQL 中每个页的大小固定为 16KB,随着数据量不断增大,单个页无法存下所有数据,就需要多个页来存储。当数据不断插入、页容量不足时,MySQL 会自动开辟新的页来保存数据,再通过双向链表的指针,把所有页串联起来。但这种组织方式带来了新的问题:页与页之间依然是通过链表连接的,查找数据时还是需要线性遍历,不断把下一个页加载到内存中进行比对,会产生大量的 IO 操作,也会导致效率降低。

为了解决多页之间的遍历效率问题,我们沿用之前 "目录" 的思路,给页与页之间也引入目录管理。这种目录和页内目录不同,它管理的不是存储数据的页,而是页本身。每个目录项由 "键值 + 指针" 构成,键值是它指向的页中存放的最小数据主键值,前后指针则指向前后存储数据的页。这样一来,我们就不用逐个遍历页了,而是可以先通过目录项快速判断目标数据可能存在哪个页中,再直接跳转到目标页,避免了不必要的页加载和遍历。

为了存放这些页目录项,MySQL 会创建专门的 "目录页",这些页不存储用户数据,只记录其他数据页的地址和最小主键信息。从上图可以看到,目录页里的每个条目,都对应一个实际存储数据的子页,比如第一个目录页里的 1 和 6,就分别是下面两个子页的最小主键编号。单个目录页可以管理近 1000 个存储数据的子页,这意味着即使数据量达到百万级、千万级,我们也只需要通过目录页的几次比对,就能快速定位到目标数据所在的页,大幅减少了 IO 次数。

其实目录页的本质也是普通的页,只是普通页里存的是用户数据,而目录页里存的是普通页的地址和键值信息。 这种 "目录页管理数据页" 的层级结构,正是 B + 树索引的核心雏形,它把原本线性的链表遍历,变成了树形结构的快速查找,这也是为什么索引能在海量数据场景下实现高效查询的根本原因。

B+树索引结构

我们接着前面的内容,来看多层目录页如何演变成 B + 树索引结构,以及它为什么能让查询效率实现质的飞跃。

随着数据量越来越大,管理数据页的目录页也会越来越多,如果我们还是线性遍历这些目录页,效率又会回到原点。这时候我们可以用同样的思路,给目录页再加上一层更高层级的目录页,最终形成一个从上到下的层级结构,也就是我们常说的 B + 树。从下图可以看到,最顶层是根目录页,中间是各级非叶子节点,最底层是存储用户数据的叶子节点页。查找数据时,我们直接从根节点往下索引,完全不需要再进行线性遍历,每次查找的页数量大幅减少,IO 次数也随之降低,查询效率自然就提上来了。

B + 树有几个关键的设计特点,正是这些特点让它成为了 MySQL 的首选索引结构。首先,非叶子节点不存储数据,只存储目录项,也就是键值和指针。这样一来,单个目录页就能存储更多的目录项,管理更多的子节点页,让整棵树保持**"矮胖型"** 的结构,树的高度会被控制得很低,查找时需要访问的页数量就会非常少,IO 次数也随之大幅减少。其次,叶子节点全部用双向链表串联起来,这是 B + 树的重要特性,能让我们高效地进行范围查询,比如查询 id 在 1 到 100 之间的数据,只需要找到起始节点,然后顺着链表往后遍历即可,不用再回到上层节点反复跳转。

我们可以总结一下 B + 树索引提升效率的核心逻辑:

  1. 每个节点都包含目录项,能让我们快速缩小查找范围,减少不必要的页访问;
  2. 矮胖型的树结构,让查找路径上的节点数量极少,需要读取的页数量也就很少;
  3. 叶子节点的链表结构,又让范围查询变得非常高效。

这种结构正是 MySQL InnoDB 引擎的索引底层实现,即使我们建表时没有设置主键,MySQL 底层也会自动生成隐藏列来构建 B + 树索引,所有的增删改查操作,都是在这个结构上完成的。

复盘一下:

首先复盘一下 B + 树索引的核心逻辑。在 B + 树结构中,Page被分为两种:目录页和数据页。目录页只存储各个下级 Page 的最小键值和指针,不存储实际数据;查找数据时,我们从顶层目录页自顶向下检索,只需要加载少量目录页到内存中,就能快速定位到目标数据所在的数据页,大幅减少了磁盘 IO 次数。

接下来我们对比一下,为什么其他数据结构不适合作为 MySQL 的索引底层:

  • 链表:只能线性遍历,每次查找都要逐个比对,效率极低,完全无法应对海量数据场景。
  • 二叉搜索树:在极端情况下会退化成线性结构,查询效率直接从 O (logn) 降到 O (n),无法保证稳定的性能。
  • AVL 树和红黑树:虽然是平衡二叉树,但本质还是二叉结构,树的整体高度会比多阶的 B + 树高很多。树越高,查询时需要访问的节点就越多,磁盘 IO 次数也会随之增加,效率不如 B + 树。
  • 哈希表:虽然等值查询的速度很快,但是面对范围查询时,效率会大幅下降,而且也无法支持排序操作,不适合数据库中常见的范围查询场景。

重点来看 B 树和 B + 树的对比,这也是最容易混淆的两种结构:

从图中可以看到,B 树的每个节点都会同时存储键值、指针和数据

而 B + 树只有叶子节点会存储完整的数据,非叶子节点只存储键值和指针。这就带来了两个关键区别:

  1. 节点存储内容不同:B 树的节点既有数据又有指针,而 B + 树的非叶子节点只存键值和指针。这样一来,B + 树的单个目录页就能存储更多的键值,能管理更多的子节点,让整棵树变得更矮,查询时需要访问的节点数更少,IO 次数也随之降低。
  2. 叶子节点的组织方式不同 :B + 树的所有叶子节点会用双向链表串联起来,而 B 树的叶子节点是不相连的。这种链表结构让 B + 树在处理范围查询时非常高效,只需要找到起始节点,就能顺着链表快速遍历整个范围,而 B 树则需要多次回溯到上层节点,效率远不如 B + 树。

这也是 MySQL InnoDB 选择 B + 树的根本原因:非叶子节点不存数据,让树更矮,IO 更少;叶子节点相连,让范围查询更高效,完美适配了数据库的使用场景。

五、聚簇索引和****非聚簇索引

我们接着前面 B + 树的话题,来看 MySQL 中两种核心的索引类型:聚簇索引和非聚簇索引,这两种索引的差异,主要体现在 MyISAM 和 InnoDB 这两个存储引擎的实现上。

MyISAM 存储引擎

MyISAM 存储引擎 --- 主键索引

我们先看 MyISAM 存储引擎的索引实现,它采用的是非聚簇索引 方案。MyISAM 同样使用 B + 树作为索引结构,但和我们之前讲的 InnoDB 不同,它的索引页和数据页是完全分离的。从上图可以看到,MyISAM 主键索引的叶子节点里,并没有存储完整的用户数据,只存放了数据记录的磁盘地址。当我们通过索引查找数据时,先通过 B + 树定位到叶子节点,拿到数据的地址,再根据这个地址去磁盘的数据页中读取完整的数据,相当于多了一次 "地址跳转" 的过程。这种把索引和数据分开存储的方式,就是非聚簇索引的核心特点。

而 InnoDB 采用的是聚簇索引 方案,和 MyISAM 完全不同。InnoDB 的主键索引本身就是聚簇索引,它把索引和用户数据存放在一起,B + 树的叶子节点里直接存储了完整的数据行,不用再通过地址去额外读取数据页。这也是我们之前讲的 B + 树结构的真实实现,查找数据时,定位到叶子节点就能直接拿到完整数据,比 MyISAM 少了一次磁盘 IO,效率更高。

这两种索引方案的本质区别,就在于索引和数据是否绑定在一起:

  1. 非聚簇索引中,索引和数据是分离的,叶子节点只存地址;
  2. 聚簇索引中,索引和数据是绑定的,叶子节点直接存数据。

这也是 MyISAM 和 InnoDB 在存储结构上最核心的差异之一。

下面我们举例说明:

我们接着通过具体的建表示例和存储文件,再深入理解两种存储引擎的差异,以及它们的辅助索引实现。

首先,我们通过建表语句来对比 InnoDB 和 MyISAM 的存储文件差异。我们创建了两个表:test1 使用 engine=innodb,test2 使用 engine=myisam。执行建表语句后,查看数据库目录下的文件,可以看到明显的区别:

  1. 对于test1(InnoDB 引擎),生成了 .frm 和 .ibd 两个文件。.frm 是表结构文件,.ibd 则是数据和索引的统一存储文件,因为 InnoDB 的聚簇索引把数据和索引都存在了这个文件里,即使表中还没有数据,这个文件也会因为存储主键索引信息而占用一定空间。
  2. 对于test2(MyISAM 引擎),生成了 .frm、.MYD 和 .MYI 三个文件。.frm 是表结构文件,.MYD 是数据文件,.MYI 是索引文件,这也对应了 MyISAM "索引与数据分离" 的非聚簇索引特性。

接下来我们看两种引擎的辅助索引(普通索引)实现。

先看 MyISAM 的辅助索引,它和主键索引的结构几乎没有区别。从图中可以看到,基于Col2建立的辅助索引,叶子节点同样存储的是数据记录的磁盘地址,和主键索引的实现方式完全一致,只是索引的键值不同而已。所以在 MyISAM 中,主键索引和辅助索引本质上是平等的,只是主键索引要求键值唯一,而辅助索引可以重复。

再看 InnoDB 的辅助索引,它的实现和主键索引(聚簇索引)完全不同。从图中可以看到,基于Col3建立的辅助索引,叶子节点里并没有存储完整的数据,只存储了对应记录的主键值。这意味着,当我们通过辅助索引查找数据时,需要执行两次索引查找:首先在辅助索引的 B + 树中找到目标记录的主键值,再拿着这个主键值去聚簇索引的 B + 树中,找到完整的数据记录,这个过程就叫做 "回表查询"。

为什么 InnoDB 的辅助索引不直接存储数据,而是要通过主键来回表查询呢?核心原因就是为了节省空间。如果每个辅助索引都存储完整的数据,会占用大量的磁盘空间,而只存储主键值,就能让辅助索引的体积变得很小,既节省了空间,也让索引的维护变得更高效。

六、总结

MySQL索引是提升数据库查询性能的关键技术,其核心原理类似书籍目录,通过有序组织数据减少磁盘IO次数。索引分为主键、唯一、普通和全文索引等类型,B+树是其主流实现结构,具有矮胖树形和叶子节点链表特性,兼顾点查与范围查询效率。InnoDB采用聚簇索引(数据与主键索引绑定)和非聚簇索引(辅助索引需回表查询)的组合方案,相比MyISAM的非聚簇设计减少了IO次数。索引通过空间换时间优化查询,但会增加写操作开销,需权衡使用。理解索引需结合磁盘硬件特性(如扇区、局部性原理)和MySQL的16KB页交互机制,其本质是通过高效数据结构改变数据组织方式,使海量数据查询从O(n)降至近似O(logn)复杂度。

谢谢大家的观看!