目录
一、索引
为什么一定要用索引?
二、为什么 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 / 红黑树)
二叉搜索树中序遍历出来是有序数组,听起来不错,但有几个硬伤:
- 最坏情况退化成链表:插入数据恰好有序时,树高变成 N,时间复杂度退化为O(N)。
- 节点个数多,树高不可控:哪怕用 AVL、红黑树保持平衡,它俩毕竟是二叉树,一个节点最多两个子节点,数据一多树还是很"高"。
- 致命的 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(不能只有一个自增列且必须定义为主键)。正确顺序是先去掉自增属性,再删主键。
七、创建索引的注意事项
- 加在高频查询的列上:索引要建在经常被 WHERE、JOIN、ORDER BY 用到的列,而不是随手建。
- 索引占额外存储空间:每个索引都是一棵 B+ 树,都要占磁盘空间。
- 增删改会同时维护索引:每次插入、更新、删除,都要同步修改索引,可能影响写性能。
- 过犹不及:创建过多或不合理的索引会导致性能下降,需要谨慎选择和规划。
关于索引的优化与使用细节(最左前缀、覆盖索引的实战、EXPLAIN 分析等),留到 MySQL 中级课程再深入。