MySQL的索引是⼀种数据结构,它可以帮助数据库高效地查询、更新数据表中的数据。索引通过一定的规则排列数据表中的记录,使得对表的查询可以通过对索引的搜索来加快速度。
MySQL索引类似于书籍的目录,通过指向数据行的位置,可以快速定位和访问表中的数据,比如汉语字典的目录(索引)页,我们可以按笔画、偏旁部首、拼音等排序的目录(索引)快速查找到需要的字。

显而易见,使用索引的目的只有⼀个,就是提升数据检索的效率,在应用程序的运行过程中,查询操作的频率远远高于增删改的频率。
时间复杂度是 O(1) ,查询速度非常快,但是MySQL并没有选择HASH做为索引的默认数据结 构,主要原因是HASH不支持范围查找
⼆叉搜索树的中序遍历是⼀个有序数组,但有几个问题导致它不适合用作索引的数据结构
AVL和红黑树,虽然是平衡或者近似平衡,但是毕竟是⼆叉结构
在检索数据时,每次访问某个节点的子节点时都会发生⼀次磁盘IO,而在整个数据库系统中,IO是性能的瓶颈,减少IO次数可以有效的提升性能
二叉搜索树的中序遍历结果是有序序列,因此天然支持范围查找 —— 这是其具备数据检索潜力的基础特性。
数据库数据存储在磁盘中,每访问一个子节点都会触发一次磁盘 IO—— 而磁盘 IO 的性能远低于内存操作,是数据库性能的核心瓶颈。若二叉搜索树高度失控(如退化为单边树),单次操作可能需要数十次甚至更多磁盘 IO,直接导致数据库性能急剧下降。
这也是数据库索引(如 MySQL InnoDB)选择 B + 树的核心原因:B + 树通过 “多叉结构” 严格控制树高,大幅减少磁盘 IO 次数,适配磁盘存储的特性。
为了解决树高的问题,我们可以使用N叉树
通过观察,相同数据量的情况下,N叉树的树高可以得到有效的控制,也就意味着在相同数据量的情况下可以减少IO的次数,从而提升效率。但是MySQL认为N叉树做为索引的数据结构还不够好。

多叉树(数据库索引常用的 B/B + 树的基础结构)如何解决二叉树的树高问题,从而适配数据库的磁盘 IO 优化需求:
二叉树每个节点最多仅 2 个子节点,数据量增大时树高会急剧上升;而多叉树允许节点包含超过 2 个子节点,能在相同数据量下大幅降低树的高度,从根源上解决树高失控的问题。
“度” 是多叉树中节点的核心参数,指单个节点最多可拥有的子节点数:
0050、0090、0130,对应 4 个子节点),实际子节点数一般小于度的值。多叉树的操作时间复杂度稳定为O(logN)(此处 log 的底数为节点的度):在数据量相同的情况下,树高被严格压缩 —— 而数据库数据存储在磁盘中,树高越低,访问目标节点所需的磁盘 IO 次数越少,因此能大幅提升数据库的查询、插入效率。
B树就是一种特殊的N叉搜索树, B+树是在B树的基础上通过“双向链表”把叶子节点串起来

B 树是一种平衡的多叉搜索树,是数据库、文件系统等磁盘存储场景中常用的索引结构,核心特点是通过 “多叉平衡” 的设计,适配磁盘 IO 的性能特性
B 树是满足以下规则的多叉树(通常用 “度(阶)” 描述节点的子节点数量上限):
m(即单个节点最多有m个子节点),则每个节点的键数介于⌈m/2⌉-1到m-1之间(保证树的平衡);O(logₘN)(N为数据总量)。特性:
B+树是⼀种经常用于数据库和文件系统等场合的平衡查找树,MySQL索引采用的数据结构,如下图所示:

在 B + 树中,节点分为非叶子节点(索引层节点)和叶子节点(数据层节点)
(1)能够保持数据稳定有序,插入与修改有较稳定的时间复杂度
(2)非叶子节点仅具有索引作用,不存储数据,所有叶子节点保真实数据
(3)所有叶子节点构成⼀个有序链表,可以按照key排序的次序依次遍历全部数据

(1)叶子节点中的数据是连续的,且相互链接,便于区间查找和搜索。
(2)非叶子节点的值都包含在叶子节点中
(3)对于B+树而言,在相同树高的情况下,查找任一元素的时间复杂度都⼀样,性能均衡。
(4)N叉树高度更低



局部性原理: 是指程序在执行时呈现出局部性规律,在⼀段时间内,整个程序的执行 仅限于程序中的某⼀部分。相应地,执行所访问的存储空间也局限于某个内存区域,局部性通常有两种形式:时间局部性和空间局部性。 时间局部性(TemporalLocality):如果⼀个信息项正在被访问,那么在近期它很可能还会被再次访问。 空间局部性(SpatialLocality):将来要用到的信息⼤概率与正在使用的信息在空间地址上是临近的。
数据页:叶子节点,存储若干个数据行 索引页 :非叶子节点, 只需要存储 key /子节点的位置
每⼀个页中即使没有数据也会使用 16KB 的存储空间,同时与索引的B+树中的节点对应,查看页的大小,可以通过系统变量 innodb_page_size 查看
SHOW VARIABLES LIKE 'innodb_page_size';
LInux 的操作系统中管理文件的大小最小的单位是4KB
MySQL作为一个数据库程序,以4KB位单位区操作文件显然就是有点小了,所以我们定义以16KB一页的默认大小。 在MySQL中有多种不同类型的页,最常用的就是用来存储数据和索引的"索引页",也叫做"数据页",但不论哪种类型的页都会包含页头(FileHeader)和页尾(FileTrailer),页的主体信息使用数 据"行"进行填充,数据页的基本结构如下图所示:


页文件头和页文件尾中包含的信息,如下图所示:
这⾥我们只关注,上⼀页页号和下⼀页页号,通过这两个属性可以把页与页之间链接起来,形成⼀个双向链表。


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

在 InnoDB 数据页中,“最小行(Infimum)” 和 “最大行(Supremum)” 的位置信息,记录的是它们相对于当前数据页起始位置的偏移量 当按主键或索引查找某条数据时,最直接简单的方法就是从头行 infimun 开始,沿着链表顺序逐个比对查找,但⼀个页有16KB,通常会存在数百行数据,每次都要遍历数百行,无法满足高效查 询,为了提高查询效率,InnoDB采用⼆分查找来解决查询效率问题;
具体实现方式是,在每⼀个页中加入⼀个叫做页目录 Page Directory 的结构,将页内包括头 行、尾行在内的所有行进行分组,约定头行(最小行)单独为⼀组,其他每个组最多8条数据,同时把每个组 最后⼀行(最大行)在页中的地址放到最后一个组了,按主键从小到大的顺序记录在页目录中在,页目录中的每⼀个位置称为⼀ 个槽,每个槽都对应了⼀个分组,⼀旦分组中的数据行超过分组的上限8个时,就会分裂出⼀个新 的分组,槽里存的都是组里的最后一个。

例如要查找主键为6的行,先比对槽中记录的主键值,定位到最后⼀个槽2,再从最后⼀个槽中的第⼀条记录遍历,第⼆条记录就是我们要查询的目标行。
利用 “上一个槽的边界行”+“行的单向链表”,锁定目标分组的范围,再遍历分组内的行找到目标记录
数据页头记录了当前页保存数据相关的信息,如下图所示

非叶子节点保存索引数据,叶子节点保存真实数据,如下图所示

以查找id为5的记录,完整的检索过程如下:
首先判断B+树的根节点中的索引记录,此时 5 < 7 ,应访问左孩子节点,找到索引页2
在索引页2中判断id的大小,找到与5相等的记录,命中,加载对应的数据页
以上的IO过程,加载索引页1-->加载索引页2-->加载数据页3
7、13→ 因为5 < 7,所以确定目标在 “左孩子节点(索引页 2)”;3、5→ 找到5对应的索引记录,这条记录直接关联了 “id=5 所在的数据页(数据页 3)” 的页号;注意:所有关于页的操作和访问都是在内存中进行的
假设⼀条用户数据大小为1KB,在忽略数据页中数据页自⾝属性空间占用的情况下,一页可以存16 条数据
索引页一条数据的大小为,主键用BIGINT类型占8Byte,下一页地址6Byte,⼀共是14Byte,⼀个索引页可以保存 16*1024/14 = 1170 条索引记录
如果只有三层树高的情况,综合只保存索引的根节点和⼆级节点的索引页以及保存真实数据的数据页,那么⼀共可以保存 1170*1170*16 = 21,902,400 条记录,也就是说在两千多万条数据的 表中,可以通过三次IO就完成数据的检索

16KB(这是 InnoDB 默认的页大小,所有数据 / 索引都以 “页” 为单位存储)。主键值:这里用的是bigint类型(MySQL 中 bigint 占 8 字节),用来做范围分界;子节点引用:指向下一个节点(下一层索引页 / 数据页)的地址(偏移量),占 6 字节;8字节(主键) + 6字节(地址)= 14字节。索引页总大小是16KB = 16 × 1024 字节 = 16384字节,用总字节数除以单条索引记录的大小,得到:16384 ÷ 14 ≈ 1170条→ 一个索引页(非叶子节点),理论上能存约 1170 条 “主键 + 子节点地址” 的索引记录。
B + 树是 “层级式” 结构,三层 B + 树的结构是:
16条实际记录(实际数据页存多少,取决于每行数据的大小;图中是举例子用 16)。所以总记录数 = 第 1 层对应第 2 层的数量 × 第 2 层对应第 3 层的数量 × 每个数据页的记录数:1170 × 1170 × 16 = 21902400条(约 2190 万条)
当在⼀个表上定义⼀个主键 PRIMARY KEY 时,InnoDB使用它作为聚集索引。
推荐为每个表定义⼀个主键。如果没有逻辑上唯一且非空的列或列集可以使用主键,则添加⼀个自增列。
最基本的索引类型,没有唯⼀性的限制。
可能为多列创建组合索引,称为复合索引或组全索引
当在⼀个表上定义⼀个唯⼀键 UNQUE 时,自动创建唯⼀索引。
与普通索引类似,但区别在于唯⼀索引的列不允许有重复值。
基于文本列(CHAR、VARCHAR或TEXT列)上创建,以加快对这些列中包含的数据查询和DML操作
用于全文搜索,仅MyISAM和InnoDB引擎支持。
CHAR、VARCHAR、TEXT)上创建,用于加速文本内容的查询与全文搜索。
-- 创建全文索引
CREATE FULLTEXT INDEX idx_content ON articles(content);
-- 使用全文索引查询
SELECT * FROM articles
WHERE MATCH(content) AGAINST('数据库 索引');聚集索引的选择规则(InnoDB)
• 聚集索引以外的索引称为⾮聚集索引或⼆级索引 ROW_ID 做为索引。
• ⼆级索引中的每条记录都包含该⾏的主键列,以及⼆级索引指定的列。
• InnoDB使用这个主键值来搜索聚集索引中的行,这个过程称为回表查询
二级索引的叶子节点存储的是:
例如,如果在 name 列上创建二级索引,索引结构大致如下:
索引键(name值) -> 主键值假设有一个 users 表,结构如下:
CREATE TABLE users (
id INT PRIMARY KEY, -- 聚集索引
name VARCHAR(50),
age INT,
INDEX idx_name (name) -- 二级索引
);执行以下查询:
SELECT * FROM users WHERE name = 'Alice';查询过程:
idx_name 中查找 name = 'Alice' 的记录,得到对应的 id 值。
id 值,在聚集索引(主键索引)中查找完整的行数据。
SELECT id, name FROM users WHERE name = 'Alice'; 只需访问二级索引 idx_name。

举例子:
SELECT * FROM table WHERE name = '张三' 为例)name 列建立的非聚集索引树中查找 name = '张三' 的记录。
id = 1001)。
id = 1001 后,会去 主键索引树(聚集索引) 中查找 id = 1001 对应的完整数据行。

这里的组合索引是按(name, sn)的顺序创建的(name为索引最左列,sn为后续列)。组合索引的生效规则是 “最左匹配原则”:
查询语句是select name from student where sn = '100002';:
(name, sn)的组合索引不会生效(即不走该组合索引),数据库无法通过该索引定位数据,会触发全表扫描(或走其他针对 sn 的独立索引,若存在)。若需让sn作为查询条件时生效,需为 sn 单独创建独立索引,而非依赖(name, sn)的组合索引。
当⼀个select语句使用了普通索引且查询列表中的列刚好是创建普通索引时的所有或部分列,这时就可以直接返回数据,而不用回表查询,这样的现象称为索引覆盖

注意:创建一个索引就会生成一个索引树,生成多少个索引就会生成多少个索引树

数据库中 B + 树的层数并非固定值,而是由数据量、索引记录大小、数据库页大小共同决定,实际应用中通常为 2~4 层:
以 InnoDB(默认页大小 16KB)为例:
bigint(8 字节)+ 子节点引用(6 字节),单条索引记录占 14 字节,单个索引页可存约 16×1024÷14≈1170条 索引记录(即 “扇出数” 约 1170)。1170 × 单数据页记录数(若单数据页存 16 条,可覆盖约 1.87 万条);1170×1170 × 单数据页记录数(约 2190 万条);1170×1170×1170 × 单数据页记录数(约 256 亿条)。B + 树的层数直接对应 “查询时需要访问的磁盘页数量”:
这里的组合索引是按(name, sn)的顺序创建的(name为索引最左列,sn为后续列)。组合索引的生效规则是 “最左匹配原则”:
查询语句是select name from student where sn = '100002';:
(name, sn)的组合索引不会生效(即不走该组合索引),数据库无法通过该索引定位数据,会触发全表扫描(或走其他针对 sn 的独立索引,若存在)。若需让sn作为查询条件时生效,需为 sn 单独创建独立索引,而非依赖(name, sn)的组合索引。
InnoDB 的 “数据存储规则” 和 “自增主键的特性” 完美匹配,从而减少性能损耗。
当插入数据时,若新数据的主键值 “插在两个已有主键中间”,且目标数据页已经满了,InnoDB 就必须做一件 “费力的事”——页分裂:
举个反例(非自增主键):
数据页1:[10, 15] (2条记录,有7个空位) 数据页2:[20,30,40,50,60,70,80,90] (8条记录,有1个空位)
自增整型主键(比如 id INT AUTO_INCREMENT)的核心特性是:插入的新主键值永远是 “当前最大的”(比如之前最大是 90,新插入就是 91,再插是 92)。
结合 InnoDB 的 “按主键排序” 规则,这种特性会导致:
InnoDB实际采用更智能的分裂策略:
原始页:[10,20,30,40,50,60,70,80,90] (已满)
插入:35(应插在30和40之间)
实际分裂过程:
1. 找到中间点:通常是页的中间位置
2. 创建新页(页2)
3. 将约一半数据移到新页(如[60,70,80,90])
4. 原页保留[10,20,30,40,50]
5. 插入35到原页:[10,20,30,35,40,50]
结果:
页1:[10,20,30,35,40,50] (6条,约66%填充)
页2:[60,70,80,90] (4条,约44%填充)-- InnoDB的填充因子配置
-- 默认PAGE_FILL_FACTOR = 15/16 = 93.75%
-- 保留6.25%空间用于更新优势:
详细的分裂点选择策略:
不考虑新插入记录的位置、不考虑每条记录的大小,仅按 原页中记录的 “条数” 找中间值 —— 比如原页有 N 条记录,分裂点就是第「N/2」条记录(向下取整或向上取整,由 InnoDB 内部逻辑决定)。
实现 “最简单的均衡”,避免代码逻辑复杂,保证分裂过程的稳定性(默认优先保证 “不出错”,再考虑优化)。
原页(16KB)存 8 条记录(主键 10、20、30、40、50、60、70、80),每条记录大小相近(假设均为 1KB):
优先考虑新插入记录的位置,把分裂点设在 “插入点附近”,让新插入记录所在的一侧(原页或新页)预留更多空间,避免短期内再次触发分裂。
针对性优化 “频繁在同一区间插入” 的场景(比如非自增主键的中间插入),减少分裂次数。
原页存 8 条记录(10、20、30、40、50、60、70、80),现在要插入主键 15(插入点在 10 和 20 之间),触发页分裂:
不按 “记录条数” 划分,而是计算原页中所有记录的 总字节数,找到 “总字节数的中间位置” 作为分裂点 —— 确保原页和新页的 “占用字节数” 接近,而非 “记录数” 接近。
解决 “记录大小差异大” 的问题(比如表中有大字段 VARCHAR、TEXT),避免按条数分裂导致的 “空间浪费” 或 “一侧过载”。
原页(16KB)存 5 条记录,记录大小差异大(单位:KB):
分裂点是第 3 条(主键 30),原页留 10、20、25(新插入)→ 总字节数 = 6+1+1=8KB;新页留 30、40、50→ 总字节数 = 1+1+6=8KB(看似均衡,但实际记录大小差异被忽略,无问题)。
总结:
