关于索引叶子块分裂方式的研究 (总结)

文章详细介绍了Oracle数据库中索引构建过程,特别是B*树的生长机制。在数据表进行DML操作时,索引需要同步更新。随着数据表容量增加,索引树可能通过9/1和5/5算法进行广度和深度拓展。10g版本修正了可能导致树快速生长的算法缺陷。文章通过实验展示了不同分裂算法对索引结构的影响,并探讨了索引重建如何优化结构。
摘要由CSDN通过智能技术生成

索引的构建过程是一个Oracle形成B*树的过程。在进行数据表DML操作的时候,索引树要随着索引列的变化进行同步的更新。当我们的数据表不断增加容量,对应的索引树也在进行不断的广度拓展和深度拓展。

 

树的生长

  当分裂导致B树索引的层数(Btree Level)增加时,我们称之为树的“生长”。当叶子节点分裂时,在其父节点上需要增加一条记录指向新节点,如果此时父节点上没有足够空间,则父节点也会发生分裂,如果如此递归下去,直到根节点也分裂,那么索引的高度就增加了。

  下图为一次9-1分裂导致的树的增长:

  上面的分裂过程中,节点Root、B5、B3和L4在数据插入前都已经饱和,当数据插入时,导致这4个节点发生连锁的分裂,最终root的分裂会分配两个新枝节点,分别为其左右枝节点,由于L4、B3、B5都是发生9-1分裂,在新分裂的数据块上没有被转移老数据,它们都被放到了新生的右枝上了。

  在每一个枝节点中,都有且只有一个左指针指向其下一层的左节点。这个指针很特殊,它存储于枝节点的头部而非数据区,其节点的键值是枝节点中唯一小于枝节点的键值数据、且不被存储。枝节点中其它的所有指针我们都称为右指针(即其节点键值大于等于枝节点的键值,且都有相应记录存储)。在节点分裂过程中,始终会保证每一个枝节点的左节点都有数据。

  由于左节点的特殊性,仅仅按照之前的分裂条件,当向左枝节点左侧插入数据时,即使其兄弟右枝节点数据区中没有数据(即只有左节点、没有右节点),它们的父节点都会分裂,在特殊情况下(所有左枝节点都饱和,但右枝节点下没有数据),索引高度会增加,但底层枝节点下很空,叶子节点很少。甚至于特殊情况下(索引数据块为2K、键值数据长度大于1K),叶子节点数可以等于索引高度。这一算法缺陷在9i及之前版本都存在,如下图所示:

  分裂前,所有左枝节点、叶子节点都已经饱和,左分裂造成连锁分裂,促成树的增长。如果键值为特殊数据、数据块为2K的话,此次分裂后,所有左节点仍然保持饱和状态——意味下一次的左插入会继续导致树的增长。

  在10g中,这个缺陷被修正了:当左枝节点已经饱和时,会先检查其兄弟右枝节点是否为空,如果为空,则将左枝节点的部分数据(5-5)转移到右枝节点,从而避免左枝节点的分裂,如下图所示:

  这一算法的修正避免了左分裂造成树的迅速增长。

 

站在Oracle逻辑存储结构的角度,Index是对应Segment在索引拓展的过程中也是一个不断拓展、分配新extent的过程。由于索引叶子节点对应是有序列值序列,那么不断插入新数据的过程,必然要伴随着索引叶子块的分裂过程。本系列中,我们就一起来研究一下分裂过程中使用的算法和相应规则。

 

1、索引叶子块分裂的两种算法

 

在索引树结构中,有两种节点是我们经常接触的:分支节点branch和叶子节点leaf。当我们不断向叶子节点块插入数据,保持叶子节点有序性的同时,必然伴随着叶子节点分裂的过程。Oracle在处理叶子节点分裂问题的时候,是使用两种算法的。

 

我们假定一个叶子节点数据块已经装满,只要有一个新的数据值插入,索引叶子块就会进行分裂过程。索引叶子块在进行分裂的时候,是依据两种算法进行。

 

ü  9/1算法”:当新插入的数据值要大于(或者小于)所有叶子节点的索引列取值的时候。Oracle会单独为这个新插入的值分配出一个新的数据块,原有的数据叶子节点依然在原位上不变;

ü  5/5算法”:当新插入的数据索引列取值在叶子块最大和最小取值中间的时候,Oracle会应用不同的算法。以该值为份额,Oracle会将索引块叶子节点分别存放在两个块中,形成半满的结构;

 

两种算法分析下,我们可以得到这样的猜想。在“9/1算法”模式下,索引块是保持一个相对较满的状态,每次分裂实际上都是将新插入的取值单独保存在新索引块里面,而原有的叶子节点保持不变。

 

在“5/5算法”模式下,索引块会保持一个半满的状态。如果每次插入的都是中间值,那么所有的叶子节点块都是维持一个数据半满的状态。

 

那么,我们就得到推论:“9/1算法”模式下,叶子块中叶子节点保存很密实,使用空间相对高效。在“5/5算法”模式下,叶子块节点保存比较稀疏,占用空间较多。

 

下面,我们就通过一个实验来证明我们的猜想推论。

 

2、实验环境准备

 

我们选择Oracle 11gR2进行测试。

 

 

SQL> select * from v$version;

 

BANNER

--------------------------------------------------------------------------------

Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - Production

PL/SQL Release 11.2.0.1.0 - Production

CORE        11.2.0.1.0         Production

 

 

作为对比,我们要准备相同的索引列基础取值与数据行的数量。所以,我们需要构建相同的数据列结构。

 

 

SQL> create table t (id1 number, id2 number);

Table created

 

SQL> create index idx_t_id1 on t(id1);

Index created

 

SQL> create index idx_t_id2 on t(id2);

Index created

 

 

下面是本实验最困难的部分,为了实现对比,我们可以对id1列插入1…n的顺序序列值。如果我们对算法的阐述是正确的,那么每次进行叶子节点分裂的时候,都会遵从“9/1算法”。

 

难点在于id2,如何让每次插入的取值在1…n的范围中,而且是依次逼近中间取值的呢?笔者编写了如下代码。

 

 

set serveroutput on size 10000;

declare

   i number;

   a number;

   val number;

begin

  

   a := 1;

   for i in 1.. 100000 loop

     

      if (i mod 2 = 0) then

         --偶数

         val := a;

         a := a + 1;

      else

         --奇数

         val := 100000 + 1 - a;

      end if;

     

      insert into t values (i, val);

     

      if (i mod 1000 = 0) then

         dbms_output.put_line('Processing : '||to_char(i));

         commit;

      end if;

      dbms_output.put_line(to_char(i)||'--'||to_char(val));

   end loop;

end;

/

 

 

代码中的val,就是进行依次逼近的过程。为了进行演示,笔者选择n=10的时候,插入序列如下:

 

 

SQL>

 

1--10

2--1

3--9

4--2

5--8

6--3

7--7

8--4

9--6

10--5

 

PL/SQL procedure successfully completed

 

 

Val的每一个取值,都是向中心进行的插入过程。

 

3、实验过程结果

 

我们选择插入10万条记录之后,查看两个索引段的信息。

 

 

SQL> select segment_name, bytes, blocks, extents from dba_segments where wner='SYS' and segment_name in ('IDX_T_ID1','IDX_T_ID2');

 

SEGMENT_NAME              BYTES     BLOCKS    EXTENTS

-------------------- ---------- ---------- ----------

IDX_T_ID1               2097152        256         17

IDX_T_ID2               4194304        512         19

 

 

应该说,效果出乎意外的好。依次插入的索引维持了一个相对小的索引段结构。而“5/5算法”插入的索引对应的数据段较大。

 

由于叶子节点个数、大小均相同。我们不难得出,idx_t_id2的结构叶子节点序列较为稀疏。而idx_t_id1结构较为实密。

 

  begin
         for i in 1..100000 loop
                  insert into T values(mod(i,200),to_char(i));
          end loop;
           commit;

  end;
  
  select segment_name, bytes, blocks, extents from dba_segments where  segment_name in ('IDX_T_ID1','IDX_T_ID2');
  
  SELECT * FROM T
  
  ALTER INDEX IDX_T_ID1 REBUILD
ONLINE ; 加不加online效果一样

 

4rebuild重构

 

如果此时,我们进行rebuild重构的话,两个结构会如何呢?

 

 

SQL> alter index IDX_T_ID2 rebuild;

 

Index altered

 

SQL> alter index IDX_T_ID1 rebuild;

 

Index altered

 

SQL> select segment_name, bytes, blocks, extents from dba_segments where wner='SYS' and segment_name in ('IDX_T_ID1','IDX_T_ID2');

 

SEGMENT_NAME              BYTES     BLOCKS    EXTENTS

-------------------- ---------- ---------- ----------

IDX_T_ID1               2097152        256         17

IDX_T_ID2               2097152        256         17

 

 

rebuild之后,两个索引段维持了紧密结构。Id1列索引没有什么大变化,而id2变小。

 

进而,笔者有这样的猜测:在进行rebuild的过程中,Oracle会不会是将所有的列值获取到,排序之后进行成树过程,这样形成的过程是遵照“9/1算法”,比较紧密。

 

5、结论

 

本篇从宏观角度讨论了索引成树过程中叶子节点的分裂问题。从笔者的角度看,Oracle的两种算法是一种优化过的思路。如果数据依次递增或者递减添加,Oracle会认为你插入中值,引发索引叶子块分裂的概率较低,所以就维持一个较为紧密的索引叶子结构。

 

但是当我们插入是无规律的,始终在数据范围内进行插入。  Oracle 认为应该避免频繁的叶子节点分裂过程,所以维持了一个相对稀疏的叶子节点序列。这样也是一种优化过程。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值