一、索引分裂
1. 什么是分裂
在开始介绍之前,我们先来搞清楚什么是索引分裂吧。“索引分裂”就是索引块的分裂,当一次DML事务操作修改了索引块上的数据,但是旧有的索引块没有足够的空间来容纳新修改的数据,那么将分裂出一个新索引块,旧有块的部分数据放到新开辟的索引块上去,这个过程就称为索引块的分裂(INDEX BLOCK SPLIT)。
如图1所示,当有新值插入到L4叶节点块的时候,此时L4叶节点块是“充满”状态,已经没有足够的空间来存储新值了,此时会在B2分支节点下,分裂出一个新的叶节点L5来存储新值。如果分支节点B2也是“充满”了呢?那就要进行分支节点的分裂,即在ROOT根节点下,分裂出一个新的分支节点出来。依此类推,如果根节点也“充满”了,则需要进行根节点的分裂。如果发生了根节点的分裂,也意味着B树的高度(BTREE LEVEL)增加了一个层次。对真正意义上的树来说,这种生长是好事,但对B树索引来说,这就不是什么好事情了,B树索引的高度需要严格控制的。
图1 新值产生索引分裂
2. 分裂的类型
从上面的介绍来说,我们大致可以将索引分裂归为三种类型:根节点分裂、分支节点分裂、叶节点分裂。当然,也可以说是两种类型,因为根节点分裂实质上一种特殊的分支节点分裂。我们首要需要关注的是其中叶节点的分裂,因为它是最频繁发生,对性能影响最直接的因素。
我们说过分裂出新节点后,会将一部分旧有的数据放到新节点上去,按照数据迁移量的比例,我们又可以将索引分裂分为两种类型:9-1分裂和5-5分裂。如果叶节点和分支节点同时发生分裂,其分裂比例的类型是相同的,即要么都是9-1分裂,要么都是5-5分裂。
q 9-1分裂:绝大部分数据还保留在旧有节点上,仅有非常少的一部分数据迁移到新节点上。
q 5-5分裂:旧节点和新节点上的数据比例几乎是持平的。
我们通常所说的索引分裂,大部分情况都指的是9-1的分裂。当事务向索引的最右侧的叶节点上插入一条大于或等于现有索引块上最大值的数据,且该索引块上不存在其他未提交的事务,如果没有足够的空间,就会发生9-1分裂。
很遗憾的是,当发生左侧节点上插入数据的时候,发生9-1分裂就会出现一些问题。如图2所示,当向左侧分支节点插入新值,即使其兄弟右侧分支节点数据区中没有数据(或者说没有右节点),它们的父节点都会发生分裂,极端情况下甚至会促使B树的高度增长,这对索引性能来说是很悲剧的,这一缺陷在10g以前的版本中都是存在的。
图2 左节点9-1分裂
从Oracle 10g开始,对于左侧节点的数据插入行为,引进了5-5分裂的方式,修正了9-1分裂造成的缺陷。如图3所示,当左侧分支节点B1已经“充满”状态,会去判断其兄弟右侧分支节点B2是否有空间,如果有,则将部分数据(5:5的比例)迁移到右侧分支节点上,这样就避免了分支节点甚至根节点的分裂。
图3 左节点5-5分裂
5-5分裂的方式也不是万能的,如果过于频繁的5-5分裂也会造成索引空间使用率不高,使得索引结构看上去像一个“虚胖子”,不够“结实’,同样会造成性能问题。
那什么时候会发生5-5分裂呢?简单地来说就是在索引需要分裂,但不能进行9-1分裂的时候就会触发5-5分裂。这听起来像一句废话,可将9-1分裂的条件反过来看,也正是5-5分裂发生的条件:
q 左侧节点发生新值插入时(新值小于索引中的最大值);
q 发生DML操作,索引块上没有足够空间分配新的ITL槽;
q 新值待插入的索引块上存在其他未提交的事务。
对比一下9-1分裂和5-5分裂的发生场景。9-1分裂通常是索引的键值是递增的,表上的事务并发量比较低,可保证新的数据块上有较大的空闲空间插入新值。5-5分裂通常是表上的事务并发度较高,操作的数据是无序的,需保证分裂的新旧数据块上有相对较大的空闲空间以容纳新事务的操作。
总体来看,不论是9-1分裂还是5-5分裂,对于性能来说,都不是什么好事。索引块的分裂意味着索引数据一定范围上的重组,其维护代价都是非常高昂的,应该尽可能地避免不必要的分裂发生。