插入一条数据,索引会有哪些变化

发布时间:2026/10/7 4:37:21
插入一条数据,索引会有哪些变化
MySQL InnoDB 已有 B 树索引插入一条新数据时索引完整变化详解前提InnoDB 是聚簇索引主键索引就是数据本身叶子节点存完整行数据二级索引叶子节点存主键值。B 树特点非叶子节点只存索引键 页指针所有数据都在叶子节点叶子节点双向链表。InnoDB 最小 IO 单元是页Page默认 16KBB 树的每个节点就是一个页所有分裂、合并都是以页为单位不是单条记录。扩展✅B 树里的所有节点根节点、所有非叶子节点、所有叶子节点每一个节点在 InnoDB 里面都单独对应一个 Page16KB 的数据页。B 树节点 InnoDB PageB 树的节点就是根节点、非叶子节点、叶子节点三者都是页。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新叶子节点新 pageP1 一部分记录挪到 P5然后把分裂键插入到父节点 AA 这个 page 里增加一条索引条目。3. 容易混淆的两个概念B 树节点逻辑概念树结构上的节点InnoDB Page物理存储概念磁盘 / 内存最小 IO 单元InnoDB 的 B 树实现逻辑上一个 B 树节点物理上就是一个 Page一一对应。⚠️一个 B 树节点占用一个 Page✅4. 两个小细节页里面的内容一个 Page 内部可以存放多条索引记录比如非叶子页存放很多索引键子页指针条目叶子页存放很多行数据。 页分裂是整个 PageB 节点满了才分裂不是 Page 里面单条记录满了。 Page 内部的多条记录是放在同一个 B 树节点里的。根节点的特殊性 初始建表插入少量数据时根节点同时也是叶子节点。 也就是一开始只有 1 个 Page这个 Page 既是 B 树的根又是叶子节点。当这个 page 存满第一次分裂之后才会真正分出上层根节点非叶子 多个叶子节点。初始状态只有 1 个 Page根 叶子。这个是很多人容易忽略的点。5. 总结版InnoDB B 树中逻辑上的根节点、非叶子节点、叶子节点每一个节点都对应物理上一个 16KB 的 Page。一个 Page 内部可以存储多条索引记录页分裂就是 B 树节点Page空间不足新建一个 Page 作为新节点拆分记录。初始阶段根节点同时是叶子节点。下面以主键聚簇索引为主讲解二级索引逻辑基本一致只是叶子存的内容不同。一、插入前先做的事情根据新记录的主键值通过 B 树从根节点往下查找根页 → 非叶子页 → 找到目标叶子页这条记录应该插入到这个页里面的哪个位置B 树叶子节点有序找到前驱和后继记录。读取这个叶子页到内存缓冲池Buffer Pool如果页不在内存触发磁盘 IO 加载。检查当前叶子页剩余空闲空间判断能不能直接塞进去。二、场景 1目标叶子页空间充足最常见→ 直接页内插入树高度不变B 树叶子页内部的记录是按索引键有序排列页内用槽数组Page Directory快速定位记录。在页内有序位置写入新记录更新页内槽数组Page Directory维护页内记录有序页的事务系统信息更新事务 ID、回滚指针MVCC写 redo log保证崩溃恢复内存页标记为脏页后续刷盘 ✅索引 B 树结构没有变化没有页分裂非叶子节点完全不动树高度不变。举例主键自增场景永远插在叶子页末尾空间够就直接追加开销最小。三、场景 2目标叶子页空间不足 → 发生【页分裂 page split】B 树会新增节点当目标叶子页剩余空间放不下新记录就要分裂3.1 叶子页分裂过程新建一个空白叶子页把原页一半左右的数据移动到新页InnoDB 默认分裂策略中间键作为分裂点将新记录插入到对应的页要么留在原页要么放到新页维护叶子节点双向链表修改前后页的指针把新叶子页串进链表把分裂点的索引键中间值插入到上层父非叶子节点。父节点记录分裂键值 指向新叶子页的指针。父节点的作用用于搜索时找到新页。3.2 父节点也可能满递归向上分裂B 树长高如果插入分裂键的时候父非叶子页空间也满了父页继续分裂继续往上一层插入分裂键一直递归直到某一层父页有空闲空间极端情况根节点也满了根页分裂新建一个根节点B 树高度 1。B 树高度一般很低百万千万数据大多 3~4 层根节点常驻内存。⚠️ 重点区分自增主键 vs 随机主键UUID自增主键新主键永远最大插入在叶子链表末尾。 页满分裂时旧页保留原有数据新页几乎是空的不会搬一半旧数据分裂代价小碎片少。随机主键UUID插入位置随机经常插到中间叶子页触发大量中间页分裂大量数据搬迁磁盘碎片变多性能差。四、二级索引的插入变化和聚簇索引逻辑相似但叶子存主键二级索引普通索引 / 唯一索引B 树叶子节点存索引列值 主键不存完整行。 插入新行时除了聚簇索引要处理每个二级索引都要单独走一遍上面的查找 插入逻辑如果二级索引页满同样触发二级索引 B 树的页分裂所以一张表索引越多插入一条数据要维护的 B 树越多插入性能越差。唯一索引额外插入前会检查索引键唯一性会加锁普通索引没有唯一性校验。五、插入后其他配套变更不属于 B 树结构但和索引绑定Undo log事务更新记录写入 undo用于 MVCC 多版本和回滚Redo log所有页修改先写 redo防止宕机丢失脏页Change Buffer变更缓冲如果要修改的二级索引页不在 Buffer Pool不会立刻加载磁盘页先把插入操作缓存在 change buffer后续异步合并刷盘。聚簇索引不会走 change buffer。这个是巨大优化大量随机 DML 场景避免频繁磁盘 IO。六、插入完成后 B 树整体变化总结表场景B 树结构变化开销叶子页空间充足无页分裂树高度不变仅页内追加记录很小叶子页满父节点有空新增 1 个叶子节点父节点增加一条索引条目树高度不变中等需要搬移一半记录叶子 父节点都满递归到根逐层分裂根节点分裂树高度 1较大极少发生七、容易踩坑的关键点页分裂不是立即刷磁盘分裂只是内存 Buffer Pool 里页结构变更脏页后续后台线程刷盘redo log 保证宕机可以恢复这个分裂操作。B 树的平衡是惰性平衡不会像二叉平衡树那样旋转只有页满了才分裂删除记录也不会立刻合并页只有页空闲空间很大时才会触发页合并避免频繁合并分裂抖动。插入一条记录只会修改一条路径上的索引页从根到目标叶子的这条路径其他分支的页完全不动。八、举个完整例子自增主键已有聚簇索引 B 树叶子页 A 存主键 1~10页剩余空间刚好放不下 id11。查找定位到叶子页 AA 空间不足触发分裂新建叶子页 B自增场景A 保留 1~10B 用来存 11 及以后将分裂点 11 写入 A 的父非叶子节点增加一条11 - 页B叶子链表 A-B插入 id11 到页 B写 redo脏页等待刷盘。 树高度不变只是多了一个叶子节点父节点多一条条目。扩展InnoDB 删除记录索引 B 树变化 页合并逻辑对比页分裂核心前置InnoDB 删除不会立刻物理删除数据也不会马上做页合并是惰性机制。和插入时页分裂的 “及时触发” 形成鲜明对比。依旧B 树节点 Page16KB操作单位是页。一、删除一条记录的完整流程聚簇索引为主二级索引同理1. 查找阶段通过 B 树从根向下检索定位到目标记录所在的叶子页加载到 Buffer Pool。2. 标记删除重点不是直接抹掉磁盘上的数据InnoDB 是 MVCC 架构不会直接把这条记录从页里擦掉。在记录的头部打上delete 标记deleted flag。记录还保留在页内行数据还在undo log 保存旧版本保证其他事务可以读到快照。✅ 此时B 树结构完全没有任何变化。叶子页里记录还在页目录 Page Directory 也不变父节点、上层所有节点都不动。只是这条记录变成 “逻辑删除”新的查询正常跳过这条标记删除的记录。3. purge 清理阶段后台异步不是删除语句立刻执行当没有任何事务需要读取这条记录的旧版本时后台 purge 线程才会过来物理移除这条被标记删除的记录重新整理页内记录压缩页内空闲空间更新页内的 Page Directory槽数组。 到这一步页里面才有了空闲空间。但依然不会马上触发页合并二、什么时候才会触发【页合并 page merge】页合并把两个相邻叶子页的数据合并到同一个 Page释放掉空出来的 PageB 树删掉一个叶子节点。InnoDB 很保守只有满足条件才合并避免频繁合并 / 分裂抖动触发条件简化版purge 完成后某个叶子页里剩余数据很少空闲空间占比很大相邻的叶子页两者的数据加起来可以塞进单个 16KB 页InnoDB 才会把这两个页的记录合并到其中一页另外一页释放回空闲页链表。合并完成后还要向上更新父非叶子节点父节点中删掉指向被释放页的那条索引条目如果父节点删除条目后变得很空父节点也不会合并重要特性InnoDB 只合并叶子节点不会合并非叶子节点。非叶子节点只会分裂不会合并。所以 B 树只长高不会因为删除而变矮。举个例子叶子页 A主键 1~10相邻叶子页 B主键 11~20。 大量删除后A 只剩下 1~3B 只剩下 11~14AB 全部记录可以放入一个 Page 触发合并把 A、B 记录全部挪到 A 页释放 B 页在 A 的父非叶子节点删除11 - B页这条索引项叶子双向链表调整去掉 B 节点。若合并之后父节点条目变少父节点保留原样不会回收。B 树高度维持不变。三、删除的几种场景索引变化汇总场景B 树 / 页变化delete 语句执行仅打上删除标记树结构完全不变页不变purge 线程清理记录页内腾出空闲空间仅页内整理记录B 树节点不变purge 后满足条件触发叶子页合并减少 1 个叶子节点父节点删除一条索引记录树高度不变四、页分裂 VS 页合并 核心对比页分裂插入页合并删除触发时机插入时页空间不足立即触发purge 清理完成后满足空间条件才触发惰性、后台作用一个页拆成两个新增叶子节点两个相邻叶子页合并成一个释放叶子节点是否作用在非叶子节点插入条目满了非叶子节点也会递归分裂树高度 1只合并叶子节点非叶子节点永远不合并树不会变矮性能开销较高数据搬迁、父节点新增索引项较高数据搬迁、父节点删除索引项很少触发碎片影响容易产生页碎片减少页数量但触发很少五、二级索引删除的差异点删除一行数据所有二级索引都要单独走一遍标记删除 → purge 逻辑二级索引页同样惰性删除满足条件才叶子页合并Change Buffer二级索引页不在内存时delete 操作会放到 Change Buffer后续异步合并到磁盘页减少随机 IO。聚簇索引不使用 Change Buffer。六、高频面试坑点总结❌ 误区delete 直接删掉 B 树上的节点。 ✅ 正解delete 只是标记删除物理清理靠 purge页合并是很后面才可能发生的事情。❌ 误区大量删除数据B 树高度会降低。 ✅ 正解不会。非叶子节点不会合并树只会长高不会变矮。❌ 误区一个页只要空了就合并。 ✅ 正解必须和相邻页的数据总和能装入一页才合并为了防止反复合并分裂比如删一条又插一条。❌ 误区purge 会马上回收页。 ✅ 正解purge 只是清理页内记录页依然属于索引只有页合并成功后页面才会被释放。七、拓展什么是页压缩、页碎片页内碎片删除记录 purge 之后页里零散的空闲空间空间不连续虽然总空闲大但放不下一条大记录。页合并可以解决一部分碎片但不能完全消除。