☰
插入一条数据,索引会有哪些变化
2026/10/7 4:37:19 网站建设 项目流程

MySQL InnoDB 已有 B + 树索引,插入一条新数据时索引完整变化详解

前提:

InnoDB 是聚簇索引,主键索引就是数据本身(叶子节点存完整行数据);

二级索引叶子节点存主键值。

B + 树特点:

非叶子节点只存索引键 + 页指针;

所有数据都在叶子节点;叶子节点双向链表。

InnoDB 最小 IO 单元是页(Page,默认 16KB),B + 树的每个节点就是一个页,所有分裂、合并都是以页为单位,不是单条记录。

扩展

✅B + 树里的所有节点(根节点、所有非叶子节点、所有叶子节点),每一个节点,在 InnoDB 里面,都单独对应一个 Page(16KB 的数据页)。

B + 树节点 = InnoDB Page;B + 树的节点就是:根节点、非叶子节点、叶子节点,三者都是页。


1. 逐个解释

① 根节点(Root Page)

  • 本质:也是一个普通的页(16KB),类型是索引页。
  • 特殊点:根页一旦创建,永远不会被删除,页号固定。就算根页满了发生分裂,会生成一个新的根页,原来的根降级变成非叶子节点。
  • 内容:存索引键 + 子页指针(子页的 page 号),不存完整行数据。

② 非叶子节点(Non-leaf Page / 内部节点)

  • 每一个非叶子节点,单独是一个 16KB 的索引页。
  • 内容:只存索引键 + 子页指针(子 page 编号),用来做路由查找,不存真实行数据。
  • 多个非叶子页可以组成多层,比如:根 → 一级非叶子 → 二级非叶子。

③ 叶子节点(Leaf Page)

  • 每一个叶子节点,单独是一个 16KB 的页。
  • 聚簇索引叶子页:存放完整行记录 + 事务信息 + 回滚指针。
  • 二级索引叶子页:存放索引列值 + 主键值。
  • 所有叶子页之间通过双向链表指针串联起来。

所以:

B + 树的每一个节点,不管是根、中间非叶子、叶子,都是独立的 InnoDB Page。

页分裂,本质就是:某个 B + 树节点(page)空间满了,新建一个 page(新节点),把记录拆分到两个 page 里。

2. 举个直观例子

[根节点Page R] ← 根,也是一个页 / \ [非叶子Page A] [非叶子Page B] ← 两个非叶子节点,各自独立页 / \ / \ [叶子P1] [叶子P2] [叶子P3] [叶子P4] ← 叶子节点,每个都是独立页
  • R、A、B、P1、P2、P3、P4:每一个都是单独 16KB Page。
  • 查找一条记录:从根 Page 加载,读到子页指针,加载对应的非叶子 Page,继续向下直到叶子 Page。
  • 如果 P1 满了,发生页分裂:新建 P5(新叶子节点,新 page),P1 一部分记录挪到 P5,然后把分裂键插入到父节点 A(A 这个 page 里增加一条索引条目)。

3. 容易混淆的两个概念

  1. B + 树节点:逻辑概念(树结构上的节点)
  2. InnoDB Page:物理存储概念(磁盘 / 内存最小 IO 单元)

InnoDB 的 B + 树实现:逻辑上一个 B + 树节点,物理上就是一个 Page,一一对应。

⚠️一个 B + 树节点,占用一个 Page✅

4. 两个小细节

页里面的内容:一个 Page 内部可以存放多条索引记录(比如非叶子页存放很多索引键+子页指针条目;叶子页存放很多行数据)。

👉 页分裂是整个 Page(B + 节点)满了才分裂,不是 Page 里面单条记录满了。 Page 内部的多条记录,是放在同一个 B + 树节点里的。

根节点的特殊性: 初始建表插入少量数据时,根节点同时也是叶子节点。 也就是一开始,只有 1 个 Page,这个 Page 既是 B + 树的根,又是叶子节点。当这个 page 存满,第一次分裂之后,才会真正分出:上层根节点(非叶子)+ 多个叶子节点。

初始状态:只有 1 个 Page,根 = 叶子。这个是很多人容易忽略的点。

5. 总结版

InnoDB B + 树中,逻辑上的根节点、非叶子节点、叶子节点,每一个节点都对应物理上一个 16KB 的 Page。一个 Page 内部可以存储多条索引记录;页分裂就是 B + 树节点(Page)空间不足,新建一个 Page 作为新节点,拆分记录。初始阶段根节点同时是叶子节点。

下面以主键聚簇索引为主讲解,二级索引逻辑基本一致,只是叶子存的内容不同。

一、插入前先做的事情

  1. 根据新记录的主键值,通过 B + 树从根节点往下查找:
    • 根页 → 非叶子页 → 找到目标叶子页:这条记录应该插入到这个页里面的哪个位置(B + 树叶子节点有序,找到前驱和后继记录)。
  2. 读取这个叶子页到内存缓冲池(Buffer Pool),如果页不在内存,触发磁盘 IO 加载。
  3. 检查当前叶子页剩余空闲空间,判断能不能直接塞进去。

二、场景 1:目标叶子页空间充足(最常见)→ 直接页内插入,树高度不变

B + 树叶子页内部的记录是按索引键有序排列,页内用槽数组(Page Directory)快速定位记录。

  1. 在页内有序位置写入新记录;
  2. 更新页内槽数组(Page Directory),维护页内记录有序;
  3. 页的事务系统信息更新:事务 ID、回滚指针(MVCC);
  4. 写 redo log(保证崩溃恢复),内存页标记为脏页,后续刷盘; ✅索引 B + 树结构没有变化,没有页分裂,非叶子节点完全不动,树高度不变。

举例:主键自增场景,永远插在叶子页末尾,空间够就直接追加,开销最小。

三、场景 2:目标叶子页空间不足 → 发生【页分裂 page split】,B + 树会新增节点

当目标叶子页剩余空间放不下新记录,就要分裂:

3.1 叶子页分裂过程

  1. 新建一个空白叶子页;
  2. 把原页一半左右的数据移动到新页(InnoDB 默认分裂策略:中间键作为分裂点);
  3. 将新记录插入到对应的页(要么留在原页,要么放到新页);
  4. 维护叶子节点双向链表:修改前后页的指针,把新叶子页串进链表;
  5. 把分裂点的索引键(中间值)插入到上层父非叶子节点。

    父节点记录:分裂键值 + 指向新叶子页的指针。父节点的作用:用于搜索时找到新页。

3.2 父节点也可能满,递归向上分裂(B + 树长高)

  • 如果插入分裂键的时候,父非叶子页空间也满了:父页继续分裂,继续往上一层插入分裂键;
  • 一直递归,直到某一层父页有空闲空间;
  • 极端情况:根节点也满了:根页分裂,新建一个根节点,B + 树高度 + 1。

B + 树高度一般很低,百万~千万数据大多 3~4 层,根节点常驻内存。

⚠️ 重点区分:自增主键 vs 随机主键(UUID)

  1. 自增主键:新主键永远最大,插入在叶子链表末尾。 页满分裂时,旧页保留原有数据,新页几乎是空的,不会搬一半旧数据,分裂代价小,碎片少。
  2. 随机主键(UUID):插入位置随机,经常插到中间叶子页,触发大量中间页分裂,大量数据搬迁,磁盘碎片变多,性能差。

四、二级索引的插入变化(和聚簇索引逻辑相似,但叶子存主键)

二级索引(普通索引 / 唯一索引)B + 树:叶子节点存索引列值 + 主键,不存完整行。 插入新行时:

  1. 除了聚簇索引要处理,每个二级索引都要单独走一遍上面的查找 + 插入逻辑;
  2. 如果二级索引页满,同样触发二级索引 B + 树的页分裂;

所以:一张表索引越多,插入一条数据要维护的 B + 树越多,插入性能越差。

唯一索引额外:插入前会检查索引键唯一性,会加锁;普通索引没有唯一性校验。

五、插入后其他配套变更(不属于 B + 树结构,但和索引绑定)

  1. Undo log:事务更新记录写入 undo,用于 MVCC 多版本和回滚;
  2. Redo log:所有页修改先写 redo,防止宕机丢失脏页;
  3. Change Buffer(变更缓冲):如果要修改的二级索引页不在 Buffer Pool,不会立刻加载磁盘页,先把插入操作缓存在 change buffer,后续异步合并刷盘。聚簇索引不会走 change buffer。

这个是巨大优化:大量随机 DML 场景,避免频繁磁盘 IO。

六、插入完成后 B + 树整体变化总结表

场景B + 树结构变化开销
叶子页空间充足无页分裂,树高度不变,仅页内追加记录很小
叶子页满,父节点有空新增 1 个叶子节点,父节点增加一条索引条目,树高度不变中等,需要搬移一半记录
叶子 + 父节点都满,递归到根逐层分裂,根节点分裂,树高度 + 1较大,极少发生

七、容易踩坑的关键点

  1. 页分裂不是立即刷磁盘:分裂只是内存 Buffer Pool 里页结构变更,脏页后续后台线程刷盘;redo log 保证宕机可以恢复这个分裂操作。
  2. B + 树的平衡是惰性平衡:不会像二叉平衡树那样旋转,只有页满了才分裂;删除记录也不会立刻合并页,只有页空闲空间很大时才会触发页合并,避免频繁合并分裂抖动。
  3. 插入一条记录,只会修改一条路径上的索引页:从根到目标叶子的这条路径,其他分支的页完全不动。

八、举个完整例子(自增主键)

已有聚簇索引 B + 树,叶子页 A 存主键 1~10,页剩余空间刚好放不下 id=11。

  1. 查找定位到叶子页 A;
  2. A 空间不足,触发分裂;
  3. 新建叶子页 B;自增场景,A 保留 1~10,B 用来存 11 及以后;
  4. 将分裂点 11 写入 A 的父非叶子节点,增加一条11 -> 页B;
  5. 叶子链表 A->B;
  6. 插入 id=11 到页 B;
  7. 写 redo,脏页等待刷盘。 树高度不变,只是多了一个叶子节点,父节点多一条条目。

扩展:

InnoDB 删除记录,索引 B + 树变化 + 页合并逻辑(对比页分裂)

核心前置:

InnoDB 删除不会立刻物理删除数据,也不会马上做页合并,是惰性机制。

和插入时页分裂的 “及时触发” 形成鲜明对比。

依旧:B + 树节点 = Page(16KB),操作单位是页。

一、删除一条记录的完整流程(聚簇索引为主,二级索引同理)

1. 查找阶段

通过 B + 树从根向下检索,定位到目标记录所在的叶子页,加载到 Buffer Pool。

2. 标记删除(重点!不是直接抹掉磁盘上的数据)

InnoDB 是 MVCC 架构:

  • 不会直接把这条记录从页里擦掉。
  • 在记录的头部打上delete 标记(deleted flag)。
  • 记录还保留在页内,行数据还在,undo log 保存旧版本,保证其他事务可以读到快照。

✅ 此时:

B + 树结构完全没有任何变化。

叶子页里记录还在,页目录 Page Directory 也不变,父节点、上层所有节点都不动。

只是这条记录变成 “逻辑删除”,新的查询正常跳过这条标记删除的记录。

3. purge 清理阶段(后台异步,不是删除语句立刻执行)

当没有任何事务需要读取这条记录的旧版本时,后台 purge 线程才会过来:

  1. 物理移除这条被标记删除的记录;
  2. 重新整理页内记录,压缩页内空闲空间;
  3. 更新页内的 Page Directory(槽数组)。

👉 到这一步,页里面才有了空闲空间。但依然不会马上触发页合并!

二、什么时候才会触发【页合并 page merge】

页合并:把两个相邻叶子页的数据合并到同一个 Page,释放掉空出来的 Page(B + 树删掉一个叶子节点)。

InnoDB 很保守:只有满足条件才合并,避免频繁合并 / 分裂抖动

触发条件(简化版):

  1. purge 完成后,某个叶子页里剩余数据很少,空闲空间占比很大;
  2. 相邻的叶子页,两者的数据加起来,可以塞进单个 16KB 页;
  3. InnoDB 才会把这两个页的记录合并到其中一页,另外一页释放回空闲页链表。

合并完成后,还要向上更新父非叶子节点:

  • 父节点中,删掉指向被释放页的那条索引条目;
  • 如果父节点删除条目后变得很空,父节点也不会合并!

重要特性:

InnoDB 只合并叶子节点,不会合并非叶子节点。

非叶子节点只会分裂,不会合并。

所以 B + 树只长高,不会因为删除而变矮。

举个例子

叶子页 A:主键 1~10;相邻叶子页 B:主键 11~20。 大量删除后:

  • A 只剩下 1~3;B 只剩下 11~14;
  • A+B 全部记录可以放入一个 Page; 触发合并:
  1. 把 A、B 记录全部挪到 A 页;
  2. 释放 B 页;
  3. 在 A 的父非叶子节点,删除11 -> B页这条索引项;
  4. 叶子双向链表调整,去掉 B 节点。

若合并之后父节点条目变少,父节点保留原样,不会回收。B + 树高度维持不变。

三、删除的几种场景,索引变化汇总

场景B + 树 / 页变化
delete 语句执行,仅打上删除标记树结构完全不变,页不变
purge 线程清理记录,页内腾出空闲空间仅页内整理记录,B + 树节点不变
purge 后满足条件,触发叶子页合并减少 1 个叶子节点,父节点删除一条索引记录;树高度不变

四、页分裂 VS 页合并 核心对比

页分裂(插入)页合并(删除)
触发时机插入时页空间不足,立即触发purge 清理完成后,满足空间条件才触发,惰性、后台
作用一个页拆成两个,新增叶子节点两个相邻叶子页合并成一个,释放叶子节点
是否作用在非叶子节点插入条目满了,非叶子节点也会递归分裂,树高度 + 1只合并叶子节点;非叶子节点永远不合并,树不会变矮
性能开销较高:数据搬迁、父节点新增索引项较高:数据搬迁、父节点删除索引项;很少触发
碎片影响容易产生页碎片减少页数量,但触发很少

五、二级索引删除的差异点

  1. 删除一行数据,所有二级索引都要单独走一遍标记删除 → purge 逻辑;
  2. 二级索引页同样惰性删除,满足条件才叶子页合并;
  3. Change Buffer:二级索引页不在内存时,delete 操作会放到 Change Buffer,后续异步合并到磁盘页,减少随机 IO。聚簇索引不使用 Change Buffer。

六、高频面试坑点总结

  1. ❌ 误区:delete 直接删掉 B + 树上的节点。 ✅ 正解:delete 只是标记删除;物理清理靠 purge;页合并是很后面才可能发生的事情。
  2. ❌ 误区:大量删除数据,B + 树高度会降低。 ✅ 正解:不会。非叶子节点不会合并,树只会长高,不会变矮。
  3. ❌ 误区:一个页只要空了就合并。 ✅ 正解:必须和相邻页的数据总和能装入一页,才合并;为了防止反复合并分裂(比如删一条又插一条)。
  4. ❌ 误区:purge 会马上回收页。 ✅ 正解:purge 只是清理页内记录,页依然属于索引,只有页合并成功后,页面才会被释放。

七、拓展:什么是页压缩、页碎片

页内碎片:删除记录 purge 之后,页里零散的空闲空间,空间不连续,虽然总空闲大,但放不下一条大记录。页合并可以解决一部分碎片,但不能完全消除。

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

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

立即咨询