☰
MySQL 索引 B + 树机制深度剖析
2026/10/20 11:01:17 网站建设 项目流程

目录

一、索引

为什么一定要用索引?

二、为什么 MySQL 偏偏选 B+树

1.HASH(哈希)

2.二叉搜索树(BST / AVL / 红黑树)

3.N 叉树

4.B+ 树:最终答案

B+ 树 vs B 树,强在哪?

三、深挖底层:MySQL 中的"页"(Page)

1.为什么要用"页"?

2.页文件头和页文件尾

3.页主体

4.页目录:从"从头遍历"到"二分查找"

5.数据页头

四、B+ 树在 MySQL 索引中的应用

1.检索过程:一次查询要走几步?​编辑

2.算一笔账:三层树高的 B+ 树能存多少数据?

五、六种索引分类

聚集索引 vs 非聚集索引,重点理解这两个概念

引出两个高频面试词:回表 与 索引覆盖

六、索引的使用:自动 vs 手动

1.自动创建

2.手动创建

3.创建复合索引(多列,逗号隔开)

4.查看索引

5.删除索引

七、创建索引的注意事项


一、索引

MySQL 的索引,本质是一种数据结构,用来帮助数据库高效地查询、更新表中的数据。它按一定规则把表里的记录"排好序",这样查询时只要在索引里找,而不是把整张表翻一遍。

最形象的类比就是字典目录:汉语字典前面有按笔画、偏旁部首、拼音排序的目录页。你要查一个"字",不会从第一页翻到最后一页,而是先翻目录,定位到这个字大概在哪一页,再跳过去。索引就扮演了这个"目录"的角色——它指向数据行所在的位置,帮我们快速定位。

为什么一定要用索引?

显而易见,使用索引的目的只有一个,就是提升数据检索的效率,在应用程序的运行过程中,查询操作的频率远远高于增删改的频率。


二、为什么 MySQL 偏偏选 B+树

1.HASH(哈希)

  • 优点:理论上查询时间复杂度是O(1),极快,等值查找无敌。
  • 缺点:查找的时候只能进行"相等"匹配,不能进行 > < 或者范围匹配

2.二叉搜索树(BST / AVL / 红黑树)

二叉搜索树中序遍历出来是有序数组,听起来不错,但有几个硬伤:

  1. 最坏情况退化成链表:插入数据恰好有序时,树高变成 N,时间复杂度退化为O(N)。
  2. 节点个数多,树高不可控:哪怕用 AVL、红黑树保持平衡,它俩毕竟是二叉树,一个节点最多两个子节点,数据一多树还是很"高"。
  3. 致命的 IO 问题:索引数据持久化存储在磁盘中,每向下访问一个子节点,就可能触发一次磁盘 IO。而磁盘读写速度远慢于内存访问,是数据库的核心性能瓶颈。树的高度越高,查询途经的节点越多,IO 次数就越多,查询性能也就越差。

3.N 叉树

为了解决树高问题,很自然的思路就是——一个节点多存几个子节点,即N 叉树。

相同数据量下,N 叉树的树高被有效压低了,也就意味着减少磁盘 IO 次数,从而提升效率。但 MySQL 认为 N 叉树还不够好,最后选择了它的"进阶版"——B+ 树。

4.B+ 树:最终答案

B+ 树是常用于数据库和文件系统的平衡查找树,也是MySQL 索引实际采用的数据结构。

B+ 树的三个核心特点:

特点

说明

数据稳定有序

能保持数据有序,插入和修改的时间复杂度稳定

非叶子只做索引

非叶子节点仅存储索引(key),不存真实数据;所有真实数据都保存在叶子节点

叶子组成有序链表

所有叶子节点构成一个有序链表,可按 key 排序依次遍历全部数据

B+ 树 vs B 树,强在哪?

对比项

B 树

B+ 树

非叶子节点

也存数据

只存索引,不存数据 → 一页能装更多 key,树更矮

叶子节点数据

分散在各层

连续且互相链接,便于区间查找和搜索

查找性能

有的层有数据、有的没有,不均匀

相同树高下查找任一元素的时间复杂度一样,性能均衡

非叶子值归属

数据只存一处

非叶子节点的值都包含在叶子节点中,查询路径统一

一句话记住:B+ 树把"索引"和"数据"彻底分离——上层只做"路标",真正的内容全部堆在底层叶子,再用链表串起来。这让它既矮(IO 少)又支持范围查询(链表好遍历)。


三、深挖底层:MySQL 中的"页"(Page)

讲 B+ 树光看树形结构是虚的,得落到 MySQL 物理存储上。这里有个关键概念——页。

1.为什么要用"页"?

页是内存与磁盘交互的最小单元,默认大小 16KB。每次内存和磁盘交互,至少读取一页。

为什么一次要读 16KB 这么大一块?核心依据是局部性原理:

  • 时间局部性:某个数据刚被访问过,近期很可能再次被访问。
  • 空间局部性:将来要用的数据,大概率与正在使用的数据在空间地址上临近。

所以一次读一整页进内存:当下次查询的数据还在这页里时,就能直接从内存读取,减少磁盘 IO,性能自然提升。

查看页大小的命令:

SHOW VARIABLES LIKE 'innodb_page_size';

在MySQL中有多种不同类型的页,最常⽤的就是⽤来存储数据和索引的"索引⻚",也叫做"数据
页",数据页的基本结构如下图所示:

2.页文件头和页文件尾

不论哪种类型的页,都包含页头(File Header)和页尾(File Trailer),主体用"数据行"填充。整体结构大概是:

  • 页头 / 页尾:包含元信息。我们只关注两个——上一页页号、下一页页号,靠它们把页与页链接起来,形成双向链表。

3.页主体

保存真实数据的主要区域。一个新页会自动分配页内最小行和页内最大行作为链表的头和尾。

中间的每行记录"下一行的地址偏移量",从而组成一个单向链表。数据按主键从小到大的顺序链接。

4.页目录:从"从头遍历"到"二分查找"

如果按主键找一条数据,最笨的方法是从头行开始沿链表逐个比对。但一个页有 16KB,通常有几百行数据,每次遍历几百行,效率太低。

InnoDB 的优化是:在每个页里加一个页目录结构——

  • 把页内所有行(含头行、尾行)分组,头行单独一组,其他每组最多8 条;
  • 把每组最后一行在页中的地址,按主键从小到大记录在页目录里;
  • 页目录里的每个位置叫一个槽,每个槽对应一个分组;组内超过 8 行就分裂出新分组。

查询某行时,先用二分查找定位到对应的槽,再在槽内最多 8 行里遍历。这样查询效率大幅提升。

举例:要找主键为 6 的行,先比对各槽记录的主键值,定位到"槽 2",再从槽 2 的第一条记录开始遍历,第二条就是目标行。

5.数据页头

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


四、B+ 树在 MySQL 索引中的应用

1.检索过程:一次查询要走几步?

在 InnoDB 里,非叶子节点保存索引数据,叶子节点保存真实数据。

假设要找 id = 5 的记录,完整流程是:

  • 先看 B+ 树根节点的索引记录,5 小于某个分界值 → 访问左孩子,加载索引页 2;
  • 在索引页 2 里判断 id 大小,找到等于 5 的记录,命中;
  • 加载对应的数据页。

三次磁盘 IO 的过程:加载索引页1 → 加载索引页2 → 加载数据页3。

2.算一笔账:三层树高的 B+ 树能存多少数据?

数据页(叶子节点):假设单行数据占 1KB,忽略页头页尾开销,一页 16KB 大约可以存 16 条完整数据。

索引页(非叶子节点):每条索引项 = 主键(BIGINT 占 8 字节)+ 下一页地址(6 字节)= 14 字节。一页 16KB 大约可以存16×1024 ÷ 14 ≈ 1170条索引记录。

于一棵三层的 B + 树(根节点 + 一层索引节点 + 叶子节点):

  • 根节点可以指向约 1170 个二级索引页;
  • 每个二级索引页可以指向约 1170 个数据页;
  • 总数据量 = 1170 × 1170 × 16 ≈2190 万条。

这就是"矮树"的回报:IO 次数基本等于树高。树矮 = IO 少 = 快。两千万数据只要 3 次 IO,性能自然起飞。


五、六种索引分类

索引类型

说明

关键点

主键索引

表定义了主键,InnoDB 就用它做聚集索引

推荐每个表都定义主键,没有合适的就用自增列

普通索引

最基本的索引,没有唯一性限制

可为多列创建,此时叫复合索引/组合索引

唯一索引

与普通索引类似,但列不允许重复值

表定义了唯一键时自动创建

全文索引

基于文本列(CHAR/VARCHAR/TEXT)创建

用于全文搜索,仅 MyISAM 和 InnoDB 支持

聚集索引

与主键索引基本是同义词

数据行的物理顺序与索引顺序一致,索引即数据

非聚集索引

聚集索引以外的索引,也叫二级索引

每条记录包含主键列 + 二级索引指定列

聚集索引 vs 非聚集索引,重点理解这两个概念

  • 聚集索引(主键索引):叶子节点直接保存真实数据行。表没有主键时,InnoDB 会用第一个非空唯一列作为聚集索引;再没有,就生成一个 6 字节、单调递增的隐藏行号当索引。
  • 非聚集索引(二级索引):叶子节点保存的是主键列 + 二级索引指定的列,而不是真实数据。
引出两个高频面试词:回表 与 索引覆盖
  • 回表查询:通过二级索引查到主键后,再用主键去聚集索引里"走一遍"拿真实数据,这个过程就叫回表。多了一次 IO。
  • 索引覆盖:如果一条 SELECT 用了普通索引,且查询的列恰好都在这个索引里,那么不用回表,直接从索引返回数据。这叫索引覆盖,是优化查询的常用手段。

实战感悟:设计索引时,如果能把高频查询的列"塞进"同一个复合索引里,让查询走索引覆盖、避免回表,是性价比非常高的优化点。


六、索引的使用:自动 vs 手动

1.自动创建

  • 给表加主键约束、外键约束、唯一约束时,MySQL 会为对应列自动创建索引。
  • 如果表没指定任何约束,MySQL 也会为每列自动生成一个索引并用行号标识。

2.手动创建

① 主键索引

# 方式一,创建表时创建主键 create table t_test_pk ( id bigint primary key auto_increment,-- 这里的 primary key (id) 同时创建了主键约束 + 主键索引 name varchar(20) ); # 方式二,创建表时单独指定主键列 create table t_test_pk1 ( id bigint auto_increment, name varchar(20), primary key (id)-- 这里的 primary key (id) 同时创建了主键约束 + 主键索引 ); # 方式三,建表后通过ALTER语句追加创建 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;

② 唯一索引

# 方式一, 创建表时创建唯一键 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) ;

③ 普通索引

# 方式一,创建表时指定索引列 create table t_test_index ( id bigint primary key auto_increment, name varchar(20) unique sno varchar(10), index(sno) ); #方式二,修改表中的列为普通索引 create table t_test_index1 ( id bigint primary key auto_increment, name varchar(20), sno varchar(10) ); alter table t_test_index1 add index (sno) ; # 方式三,单独创建索引并指定索引名 create table t_test_index2 ( id bigint primary key auto_increment, name varchar(20), sno varchar(10) ); create index index_name on t_test_index2(sno);

3.创建复合索引(多列,逗号隔开)

创建语法与创建普通索引相同,只不过指定多个列,列与列之间用逗号隔开

# 方式一,创建表时指定索引列 create table t_test_index4 ( id bigint primary key auto_increment,-- 主键索引(单字段,不是复合) name varchar(20), sno varchar(10), class_id bigint, index (sno, class_id)-- 这里就是复合索引:一个索引包含 sno + class_id 两个字段 ); # 方式二,修改表中的列为复合索引 create table t_test_index5 ( id bigint primary key auto_increment, name varchar(20), sno varchar(10), class_id bigint ); alter table t_test_index5 add index (sno, class_id); # 方式三,单独创建索引并指定索引名 create table t_test_index6 ( id bigint primary key auto_increment, name varchar(20), sno varchar(10), class_id bigint ); create index index_name on t_test_index6 (sno, class_id);--index_name复合索引有sno, class_id 2个字段

4.查看索引

# 方式一:show keys from 表名 show keys from t_test_index6; # 方式二 show index from t_test_index6; # 方式三,简要信息:desc 表名; desc t_test_index6;

5.删除索引

①删除主键(注意:若列有自增属性需先去掉)

alter table 表名 drop primary key;

例子:

②删除其他索引

alter table 表名 drop index 索引名;

踩坑提醒:删除主键时如果该列是 AUTO_INCREMENT,会报错 ERROR 1075(不能只有一个自增列且必须定义为主键)。正确顺序是先去掉自增属性,再删主键。


七、创建索引的注意事项

  1. 加在高频查询的列上:索引要建在经常被 WHERE、JOIN、ORDER BY 用到的列,而不是随手建。
  1. 索引占额外存储空间:每个索引都是一棵 B+ 树,都要占磁盘空间。
  1. 增删改会同时维护索引:每次插入、更新、删除,都要同步修改索引,可能影响写性能。
  1. 过犹不及:创建过多或不合理的索引会导致性能下降,需要谨慎选择和规划。

关于索引的优化与使用细节(最左前缀、覆盖索引的实战、EXPLAIN 分析等),留到 MySQL 中级课程再深入。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询