Btree索引

 

  索引通常可以分为:分区和非分区索引、常规B树索引、位图(bitmap)索引、翻转(reverse)索引等。其中,B树索引属于最常见的索引

B树索引是一个典型的树结构,其包含的组件主要是:
  1) 叶子节点(Leaf node):数据行的键值(key value)、键值对应数据行的 ROWID。
  2) 分支节点(Branch node):最小的键值前缀(minimum key prefix),用于在(本块的)两个键值之间做出分支选择,指向包含所查找键值的子块(child block)的指针()所有的 键值-ROWID 对(key     and ROWID pair)都与其左右的兄弟节点(sibling)向链接(link),并按照(key,ROWID)的顺序排序
  3) 根节点(Root node):一个B树索引只有一个根节点,它实际就是位于树的最顶端的分支节点。

 

B树索引的访问

当oracle进程需要访问数据文件里的数据块时,oracle会有两种类型的I/O操作方式:
  1) 随机访问,每次读取一个数据块(通过等待事件“db file sequential read”体现出来)。
  2) 顺序访问,每次读取多个数据块(通过等待事件“db file scattered read”体现出来)。
  第一种方式则是访问索引里的数据块,而第二种方式的I/O操作属于全表扫描。这里顺带有一个问题,为
  何随机访问会对应到db file sequential read等待事件,而顺序访问则会对应到db file scattered read等待事件呢?这似乎反过来了,随机访问才应该是分散(scattered)的,而顺序访问才应该是顺序     (sequential)的。其实,等待事件主要根据实际获取物理I/O块的方式来命名的,而不是根据其在I/O子系统的逻辑方式来命名的。下面对于如何获取索引数据块的方式中会对此进行说明。
  事实上在B树索引虽然为一个树状的立体结构,但其对应到数据文件里的排列当然还是一个平面的形式,也就是像下面这样。
  /根/分支/分支/叶子/…/叶子/分支/叶子/叶子/…/叶子/分支/叶子/叶子/…/叶子/分支/.....
  因此,当oracle需要访问某个索引块的时候,势必会在这个结构上跳跃的移动。
  当oracle需要获得一个索引块时,首先从根节点开始,根据所要查找的键值,从而知道其所在的下一层的分支节点,然后访问下一层的分支节点,再次同样根据键值访问再下一层的分支节点,如此这般,最  终访问到最底层的叶子节点。可以看出,其获得物理I/O块时,是一个接着一个,按照顺序,串行进行的。在获得最终物理块的过程中,我们不能同时读取多个块,因为我们在没有获得当前块的时候是不知道  接下来应该访问哪个块的。因此,在索引上访问数据块时,会对应到db file sequential read等待事件,其根源在于我们是按照顺序从一个索引块跳到另一个索引块,从而找到最终的索引块的。
  那么对于全表扫描来说,则不存在访问下一个块之前需要先访问上一个块的情况。全表扫描时,oracle知道要访问所有的数据块,因此唯一的问题就是尽可能高效的访问这些数据块。因此,这时oracle可以  采用同步的方式,分几批,同时获取多个数据块。这几批的数据块在物理上可能是分散在表里的,因此其对应到db file scattered read等待事件。

 

DML对B树索引的影响

 

INSERT

 

在每个INSERT操作过程中,关键字必须被插入在正确叶节点的位置。如果叶节点已满,不能容纳更多的关键字,就必须将叶节点拆分。拆分的方法有两种:

 

  1)如果新关键字值在所有旧叶节点块的所有关键字中是最大的,那么所有的关键字将按照99:1的比例进行拆分,使得在新的叶节点块中只存放有新关键字,而其他的所有关键字(包括所有删除的关键字)仍然保存在旧叶节点块中。
  2)如果新关键字值不是最大的,那么所有的关键字将按照50:50的比例进行拆分,这时每个叶节点块(旧与新)中将各包含原始叶节点中的一半关键字。
这个拆分必须通过一个指向新叶节点的新入口向上传送到父节点。如果父节点已满,那么这个父节点也必须进行拆分,并且需要将这种拆分向上传送到父节点的父节点。这时,如果这个父节点也已满,将继续进行这个过程。这样,某个拆分可能最终被一直传送到根节点。如果根节点满了,根结点也将进行分裂。根结点在进行分裂的时候,就是树的高度增加的时候。根节点进行分裂的方式跟其他的的节点分裂的方式相比较,在物理位置上的处理也是不同的。根节点分裂时,将原来的根结点分裂为分支节点或叶节点,保存到新的块中,而将新的根节点信息保存到原来的根结点块中,这样做的是为因为避免修改数据字典所带来的相对较大的开销。
  注意:现在Oracle都是采用了平衡算法,正常情况下即使索引关键字不断增大,也不会产生不平衡树。当索引关键字不断增大,导致树级别单方向增长时,Oracle会自动进行索引翻转以维持索引的平衡,当然这种操作非常消耗资源
在索引的每一个层次之间,每一个层最左边的节点的block头部都有一个指向下层最左边的块的指针,这样有利于fast full scan 的快速定位最左边的叶子节点。
每个拆分过程都是要花费一定的开销的,特别是要进行物理硬盘I/O动作。此外,在进行拆分之前,Oracle必须查找到一个空块,用来保存这个拆分。可以用以下步骤来进行查找空块的动作:
  1) 在索引的自由列表(free-list, 又称为空闲列表) 中查到一个空闲块,可以通过CREATE/ALTER INDEX命令为一个索引定义多个空闲列表。索引空闲列表并不能帮助Oracle查找一个可用来存放将要被插入的新关键字的块。这是因为关键字值不能随机地存放在索引中可用的第一个“空闲”叶节点块中,这个值必须经过适当的排序之后,放置在某个特定的叶节点块中。只有在块拆分过程中才需要使用索引的空闲列表,每个空闲列表都包含有一个关于“空”块的链接列表。当为某个索引定义了多个空闲列表时,首先将从分配给进程的空间列表中扫描一个空闲块。如果没有找到所需要的空闲块,将从主空闲列表中进行扫描空闲块的动作。
  2) 如果没有找到任何空闲块,Oracle将试图分配另一个扩展段。如果在表空间中没有更多的自由空间,Oracle将产生错误ORA-01654。
  3) 如果通过上述步骤,找到了所需的空闲块,那么这个索引的高水位标(HWM)将加大。
  4) 所找到的空闲块将用来执行拆分动作。
在创建B*树索引时,一个需要注意的问题就是要避免在运行时进行拆分,或者,要在索引创建过程中进行拆分(“预拆分”),从而使得在进行拆分时能够快速命中,以便避免运行时插入动作。当然,这些拆分也不仅仅局限于插入动作,在进行更新的过程中也有可能会发生拆分动作。

UPDATE
  索引更新完全不同于表更新,在表更新中,数据是在数据块内部改变的(假设数据块中有足够的空间来允许进行这种改变);但在索引更新中,如果有关键字发生改变,那么它在树中的位置也需要发生改变。请记住,一个关键字在B*树中有且只有一个位置。因此,当某个关键字发生改变时,关键字的旧表项必须被删除,并且需要在一个新的叶节点上创建一个新的关键字。旧的表项有可能永远不会被重新使用,这是因为只有在非常特殊的情况下, Oracle才会重用关键字表项槽,例如,新插入的关键字正好是被删除的那个关键字(包括数据类型、长度等等)。(这里重用的是块,但完全插入相同的值的时候,也不一定插入在原来的被删除的位置,只是插入在原来的块中,可能是该块中的一个新位置。也正因为如此,在索引块中保存的的记录可能并不是根据关键字顺序排列的,随着update等的操作,会发生变化。)那么,这种情况发生的可能性有多大呢?许多应用程序使用一个数列来产生NUMBER关键字(特别是主关键字)。除非它们使用了RECYCLE选项,否则这个数列将不会两次产生完全相同的数。这样,索引中被删除的空间一直没有被使用。这就是在大规模删除与更新过程中,表大小不断减小或至少保持不变但索引不断加大的原因。

 

 

DELETE

 

当删除表里的一条记录时,其对应于索引里的索引条目并不会被物理的删除,只是做了一个删除标记。当一个新的索引条目进入一个索引叶子节点的时候,oracle会检查该叶子节点里是否存在被标记为删除的索引条目,如果存在,则会将所有具有删除标记的索引条目从该叶子节点里物理的删除。
当一个新的索引条目进入索引时,oracle会将当前所有被清空的叶子节点(该叶子节点中所有的索引条目都被设置为删除标记)收回,从而再次成为可用索引块。
尽管被删除的索引条目所占用的空间大部分情况下都能够被重用,但仍然存在一些情况可能导致索引空间被浪费,并造成索引数据块很多但是索引条目很少的后果,这时该索引可以认为出现碎片。而导致索引出现碎片的情况主要包括:
  1、不合理的、较高的PCTFREE。很明显,这将导致索引块的可用空间减少。
  2、索引键值持续增加(比如采用sequence生成序列号的键值),同时对索引键值按照顺序连续删除,这时可能导致索引碎片的发生。因为前面我们知道,某个索引块中删除了部分的索引条目,只有当有键值进入该索引块时才能将空间收回。而持续增加的索引键值永远只会向插入排在前面的索引块中,因此这种索引里的空间几乎不能收回,而只有其所含的索引条目全部删除时,该索引块才能被重新利用。
  3、经常被删除或更新的键值,以后几乎不再会被插入时,这种情况与上面的情况类似。