虽然一级或两级索引通常有助于加快查询,但在商用系统中常使用一种更通用的结构。这一通用的数据结构簇称为B树,而最常使用的变体称为B+树。实质上:

B树能自动地保持与数据文件大小相适应的索引层次。

对所使用的存储块空间进行管理,使每个块的充满程度在半满与全满之间。这样的索引不再需要溢出块。

在接下来的内容中,我们将讨论“B树”,但具体细节都针对B+树这一变体。其他类型的B树在习题中讨论。

1、B树的结构

正如其名称所暗示的那样,B树把它的存储块组织成一棵树。这棵树是平衡的,即从树根到树叶的所有路径都一样长。通常B树有三层:根、中间层和叶,但也可以是任意多层。为了对B树有一个直观的印象,可以先看一下图4-21、图4-22和图4-23,其中前两个图所示的为B树结点,而后一个图所示的为一个小而完整的B树。


对应于每个B树索引都有一个参数n,它决定了B树的所有存储块的布局。每个存储块存放n个查找键值和n+1个指针。在某种意义上讲,B树的存储块类似于4.1节讲述的索引块,只不过B树的块除了有n个键-指针对外,还有一个额外的指针。在存储块能容纳n个键和n+1个指针的前提下,我们把n取得尽可能大。

例4.19 假定我们的存储块大小为4096字节,且整数型键值占4字节,指针占8字节。要是不考虑存储块块头信息所占空间,那么我们希望找到满足4n+8(n+1)≤4096的最大整数值n。这个值是n=340。

下面几个重要的规则限制B树的存储块中能出现的东西:

(1)根结点中至少有两个指针被使用。所有指针指向位于B树下一层的存储块。

(2)叶结点中,最后一个指针指向它右边的下一个叶结点存储块,即指向下一个键值大于它的块。在叶块的其他n个指针当中,至少有个指针被使用且指向数据记录;未使用的指针可看作空指针且不指向任何地方。如果第i个指数被使用,则指向具有第i个键值的记录。在内层结点中,所有的n+1个指针都可以用来指向B树中下一层的块。其中至少个指针被实际使用(但如果是根结点,则不管n多大都只要求至少两个指针被使用)。如果j个指针被使用,那该块中将有j-1个键,设为K1,K2,Kj-1。第一个指针指向B树的一部分,一些键值小于K1的记录可在这一部分找到。第二个指针指向B树的另一部分,所有键值大小等于K1且小于K2的记录可在这一部分中。依此类推。最后,第j个指针指向B树的又一部分,一些键值大于等于Kj-1的记录可以在这一部分中找到。注意:某些键值远小于K1或远大于Kj-1的记录可能根本无法通过该块到达,但可通过同一层的其他块到达。

(3)假若我们以常规的画树方式来画B树,任一给定结点的子结点按从左(第一个子结点)到右(最后一个子结点)的顺序排列。那么,我们在任何一个层次上从左到右来看B树的结点,结点的键值将按非减的顺序出现。

092750582.jpg


例4.20 在这个例子和其他B树实例中,我们设n=3。也就是说,块中可存放3个键值和4个指针,这是一个不代表通常情况的小数字。键值为整数。图4-21所示为一个完全使用的叶结点。其中有三个键值57、81和95。前三个指针指向具有这些键值的记录。而最后一个指针,指向右边键值大于它的下一个叶结点,这正是叶结点中通常的情况。如果该叶结点是序列中的最后一个,则该指针为空。叶结点不必全部充满,但在我们这个例子中,n=3,故叶结点至少要有两个键-指针对。也就是说,图4-21中的键值95和第三个指针可以没有,该指针标有“至键值为95的记录”。

092806220.jpg

图4-22所示为一个典型的内部结点。其中有三个键值,与我们在叶结点的例子中所选的一样:57、81和95。该结点中还有四个指针。第一个指针指向B树的一部分,通过它我们只能到达键值小于第一个键值即57的那些记录。第二个指针通向键值介于该B树块第一个键值和第二个键值之间的那些记录,第三个指针对应键值介于该块第二个键值和第三个键值之间的那些记录,第四个指针将我们引向键值大于该块中第三个键值的那些记录。同叶结点的例子一样,内部结点的键和指针槽也没有必要全部占用。不过,当n=3时,一个内部结点至少要出现一个键和两个指针。元素缺失最极端的情形就是键值只有57,而指针也仅使用前两个,在这种情况下,第一个指针对应于小于57的键值,而第二个指针对应于大于等于57的键值。键值


例4.21 图4-23所示为一棵完整的三层B+树;其中使用例4.20中所描述的结点。我们假定数据文件的记录的键是2~47之间的所有素数。注意,这些值在叶结点中按顺序出现一次。所有叶结点都有两个或3个键-指针对,还有一个指向序列中下一叶结点的指针。当我们从左到右去看叶结点时,所有键都是排好序的。根结点仅有两个指针,恰好是允许的最小数目,尽管至多可有4个指针。根结点中的某个键将通过第一个指针访问到的键值与通过第二个指针访问到的键值分隔开来。也就是说,不超过12的键值可通过根结点的第一个子树找到;大于等于13的键值可通过第二个子树找到。

092839636.jpg

图4-23B+树

如果我们看根结点的第一个具有键值7的子结点,会发现它有两个指针,一个通向小于7的键,而另一个通向大于等于7的键。注意,该结点的第二个指针只能使我们找到键7和11,而非所有大于7的键,比如键13(虽然我们可以通过叶结点中指向下一个块的指针找到那些更大的键)。

最后,根结点的第二个子结点的4个指针槽都被使用。第一个指针将我们引向一些键值小于23的键,即13、17和19。第二个指针将我们引向键值大于等于23而小于31的所有键;第三个指针将我们引向键值大于等于31而小于43的所有键;而第四个指针将我们引向一些键值大于等于43的键(在这个例子中,是所有的键)。


2、B树的应用

B树是用来建立索引的一种强有力的工具。它的叶结点上指向记录的一系列指针可以起到我们在4.1节和4.2节学过的任何一种索引文件中指针序列的作用。下面是一些实例:

1)B树的查找键是数据文件的主键,且索引是稠密的。也就是说,叶结点中为数据文件的每一个记录设有一个键-指针对。该数据文件可以按主键排序,也可以不按主键排序。

2)数据文件按主键排序,且B+树是稀疏索引,在叶结点中为数据文件的每个块设有一个键—指针对。

3)数据文件按非键属性排序,且该属性是B+树的查找键。叶结点中为数据文件里出现的每个属性值K设有一个键-指针对,其中指针指向排序键值为K的记录中的第一个。

B树变体的另一些应用允许叶在结点上查找键重复出现。图4-24所示即为这样一棵B树。这一扩展类似于我们在4.1.5节讨论的带重复键的索引。


如果我们确实允许查找键的重复出现,就需要稍微修改对内部结点中键的涵义的定义,我们曾在4.3.1节中讨论过这一定义。现在,假定一个内部结点的键为K1,K2,Kn,那么Ki将是从第i+1个指针所能访问的子树中出现的最小新键。这里的“新”,是指在树中第i+1个指针所指向的子树以左没有出现过Ki,但Ki在第i+1个指针指向的子树中至少出现一次。注意,在某些情况下可能不存在这样的键,这时Ki可以为空,但它对应的指针仍然需要,因为它指向树中碰巧只有一个键值的那个重要的部分。

092859725.jpg


例4.22 图4-24所示的B树类似于图4-23,但有重复键值。具体来说:键11已被键13替换;键19、29和31全部被键23替换。这样就造成根结点的键是17而不是13。原因在于,虽然13是第二个子树根结点中最小的键,但它不是该子树的新键,因为它在根结点的第一个子树中出现过。

我们还需要对根结点的第二个子结点做些改变。第二个键改为37,因为它是第三个子结点(从左数第五个叶结点)的第一个新键。最有趣的是,第一个键现在为空,因为第二个子结点(第四个叶结点)根本就没有新键。换言之,如果查找某个键且到达根结点的第二个子结点,我们不会从该子结点的第二个子结点起开始查找。若是查找23或其他更小的值,我们应该从它的第一个子结点起开始查找,在那里我们将找到所需的记录(如果是17),或找到所需的记录的第一个(如果是23)。

注意:

(1)查找13时我们不会到达根结点的第二个子结点,而是直接到第一个子结点中去查找。

(2)如果查找介于24~36之间的某个键,我们会直接到第三个叶结点中查找。但当我们连一个所需键值都找不到时,我们就知道不必继续往右查找。举例来说,如果叶结点中存在键24,它要么在第四个叶结点上,这时根结点的第二个子结点中的空键将会被24替代;要么在第五个叶结点上,这时根结点的第二个子结点的键37将被24替代。


3、B树中的查找

我们现在再回到最初的假定,即叶结点中没有重复键。这个假定可以简化对B树操作的讨论,但该假定对这些操作来说并非必不可少。假设我们有一个B树索引并且想找出查找键值为K的记录。我们从根到叶递归查找,查找过程为:

基础:若处于叶结点,我们就在其键值中查找。若第i个键是K,则第i个指针可让我们找到所需记录。

归纳:若处于某个内部结点,且它的键为K1,K2,,Kn,则依据在4.3.1节中给出的规则来决定下一步该对此结点的哪个子结点进行查找。也就是说,只有一个子结点可使我们找到具有键K的叶结点。如果K

例4.23 假定有一棵如图4-23所示的B树,且我们想找到查找键为40的记录。我们从根结点开始,其中有一个键13。因为13≤40,我们就沿着它的第二个指针来到包含键为23、31和43的第二层结点。

在这个结点中,我们发现31≤40<43,因而我们沿着第三个指针来到包含31、37和40的叶结点,如果数据文件中有键值为40的记录,我们就应该在这个叶结点中找到键40。既然我们没有发现键40,就可以断定在底层的数据块中没有键值为40的记录。

注意,要是查找键为37的记录,我们所做的决定都和上面一样,但当到达叶结点时,我们将找到键37。因为它是叶结点中的第二个键,因此我们沿着第二个指针可以找到键值为37的数据记录。


4、范围查询

B树不仅对搜寻单个查找键的查询很有用,而且对查找键值在某个范围内的查询也很有用。一般来说,范围查询在Where子句中有一个项,该项将查找键与单个值或多个值相比较,可用除“=”和“<>”之外的其他比较操作符。使用查找键属性K的范围查询例子如下:

select * form R where R.k > 40;

或者

select * form R where R.k > 10 and R.k <=25;

如果想在B树叶结点上找出在范围[a,b]之间的所有键值,我们通过一次查找来找出键a。不论它是否存在,我们都将到达可能出现a的叶结点,然后在该叶结点中查找键a或大于a的那些键。我们所找到的每个这样的键都有一个指针指向相应的记录,这些记录的键在所需的范围内。

如果我们没有发现大于b的键,我们就使用当前叶结点指向下一个叶结点的指针,并继续检查键和跟踪相应指针,直到我们

1)找到一个大于b的键,这时我们停止查找;或者

2)到了叶结点的末尾,在这种情况下,我们到达下一个叶结点且重复这个过程。

上面的查找算法当b为无穷时也有效;即项中只有一个下界而没有上界。在这种情况下,我们查找从键a可能出现的叶结点开始到最后一个叶结点的所有叶结点。如果a为-∞(即项中有一个上界而没下界),那么,在查找“负无穷”时,不论处于B树的哪个结点,我们总被引向该结点的第一个子结点,即最终将找到第一个叶结点。然后按上述过程查找,仅在超过键b时停止查找。

例4.24 假定我们有一棵如图4-23所示的B树,给定查找范围是(10,25)。我们查找键10,找到第二个叶结点,它的第一个键小于10,但第二个键11大于10。我们沿着它的相应指针找到键为11的记录。因为第二个叶结点中已没有其他的键,我们沿着链找到第三个叶结点,其键为13,17和19。这些键都小于或等于25,因此我们沿着它们的相应指针检索具有这些键的记录。最后,移到第四个叶结点,在那里我们找到键23。而该叶结点的下一个键29超过了25,因而已完成我们的查找。这样,我们就检索出了键11到键23的五个记录。

5、B树的插入

当我们考虑如何插入一个新键到B树时,就会发现B树优于4.1.4节介绍的简单多级索引的一些地方。对应的记录可使用4.1节中介绍的任何方法插入到建有B树索引的数据文件中;这时,我们只考虑B树如何相应地修改。插入原则上是递归的:

(1)设法在适当的叶结点中为新键找到空闲空间,如果有的话,就把键放在那里。

(2)如果在适当的叶结点中没有空间,就把该叶结点分裂成两个,并且把其中的键分到这两个新结点中,使每个新结点有一半或刚好超过一半的键。

(3)某一层的结点分裂在其上一层看来,相当于是要在这一较高的层次上插入一个新的键-指针对。因此,我们可以在这一较高层次上逆归地使用这个插入策略:如果有空间,则插入;如果没有,则分裂这个父结点且继续向树的高层推进。

(4)例外的情况是,如果试图插入键到根结点中并且根结点没有空间,那么我们就分裂根结点成两个结点,且在更上一层创建一个新的根结点。这个新的根结点有两个刚分裂成的结点作为它的子结点。回想一下,不管n(结点中键的槽数)多大,根结点总是允许只有一个键和两个子结点。


当我们分裂结点并在其父结点中插入时,需要小心地处理键。首先,假定N是一个容量为n个键的叶结点,且我们正试图给它插入第(n+1)个键和它相应的指针。创建一个新结点M,该结点将成为N结点的兄弟,紧挨在N的右边。按键排序顺序的前个键-指针对保留在结点N中;而其他的键-指针对移到结点M中,注意,结点M和结点N中都有足够数量的键-指针对,即至少有个这样的键—指针对。

现在,假定N是一个容量为n个键和n+1个指针的内部结点,并且由于下层结点的分裂,N正好又被分配给第(n+2)个指针。我们执行下列步骤:

1)创建一个新结点M,它将是N结点的兄弟,且紧挨在N的右边。

2)按排序顺序将前[(n+1)/2]个指针留在结点n中,而把剩下的[(n+1)/2]个指针移到结点M中。

3)前[n/2]个键保留在结点N中,而后[n/2]个键移到结点M中。注意,在中间的那个键总是被留出来,它既不在结点N中也不在结点M中。这一留出的键K指明通过M的第一个子结点可访问到最小键。尽管K不出现在N中也不出现在M中,但它代表通过M能到达的最小键值,从这种意义上来说它与M相关联。因此,K将会被结点N和M的父结点用来划分在这两个结点之间的查找。

例4.25 让我们在图4-23所示的B树中插入键40。根据4.3.3节中的查找过程找到插入的叶结点。如例4.23一样,我们找到第五个叶结点来插入。由于n=3,而该叶结点现在有四个键-指针对:31、37、40和41,所以我们需要分裂这个叶结点。首先是创建一个新结点,并把两个最大的键40和41以及它们的指针移到新结点。图4-25表示了这个分裂。

092926239.jpg

注意,虽然我们现在把这些结点显示在四排,但对树而言还是只有三层,而七个叶结点占据了图中的后两排。这些叶结点通过各自的最后一个指针链接起来,仍形成了一条从左到右的链。

我们现在必须插入一个指向新叶结点(具有键40和41的那个结点)的指针到它上面的那个结点(具有键23、31和43的那个结点),还必须把该指针与键40关联起来,因为键40是通过新叶结点可访问到的最小键。很不巧,分裂结点的父结点已满,它没有空间来存放别的键或指针。因此,它也必须分裂。

我们开始先找到指向后五个叶结点的指针和表示这些叶结点中后四个的最小键的键列表。也就是说,我们有指针P1、P2、P3、P4和P5指向这些叶结点,它们的最小键分别是13、23、31、40和43,并且我们用键序列23、31、40和43来分隔这些指针。前三个指针和前两个键保留在被分裂的内部结点中;而后两个指针和后一个键放到一个新结点中。剩下的键是40,表示通过新结点可访问到最小键。

插入键40后的结果如图4-26所示。根结点现在有三个子结点,最后两个是分裂的内部结点。注意,键40标志着通过分裂结点的第二个子结点可访问到的最小键,它被安置在根结点中,用来区分根结点的第二个子结点和第三个子结点的键。

092952144.jpg

6、B树的删除

如果我们要删除一个具有给定键K的记录,必须先定位该记录和它在B树叶结点中的键-指针对。正如第3节所述,这部分删除过程主要是查找。然后我们删除记录本身并从B树中删除它的键-指针对。

如果发生删除的B树结点在删除后至少还有最小数目的键和指针,那就不需要再做什么。但是,结点有可能在删除之前正好具有最小的充满度,因此在删除后,对键数目的约束就被违背了。这时,我们需要为这个键的数目仅次于最小数目的结点N做下面两件事之一,其中有一种情况需要沿着树往上递归地删除。

1)如果与结点N相邻的兄弟中有一个键和指针超过最小数目,那么它的一个键-指针对可以移到结点N中并保持键的顺序。结点N的父结点的键可能需要调整以反映这个新的情况。例如,如果结点N的右边兄弟M可提供一个键和指针,那么从结点M移到结点N的键一定是结点M的最小键。在结点M和结点N的父结点处有一个表示通过M可访问到的最小键,该键必须被提升。

2)最困难的情况是当相邻的两个兄弟中没有一个能提供额外的键给结点N时。不过,在这种情况下,我们有结点N和它的一个兄弟结点M,其中一个的键数少于最小数,而另一个的键数刚好为最小数。因此,它们合在一起也没有超过单个结点所允许的键和指针数(这就是为什么B树结点最小允许的充满程度为一半的原因)。我们合并这两个结点,实际上就是删除它们中的一个。我们需要调整父结点的键,然后删除父结点的一个键和指针。如果父结点现在足够满,那我们就完成了删除操作,否则,我们需要在父结点上递归地运用这个删除算法。

例4.26 让我们从图4-23所示最初的B树开始,即在键40插入之前。假定我们删除键7。该键在第二个叶结点中被找到。我们删除该键、该键对应的指针以及指针指向的记录。

093017420.jpg

不巧的是,第二个叶结点现在只剩下一个键,而我们需要每个叶结点至少有两个键。但该结点左边的兄弟,即第一个叶结点,有一个额外的键-指针对,这就帮了我们的大忙。我们因此可以将它的最大键以及它的相应指针移到第二个叶结点。产生的B树如图4-27所示。注意,因为第二个叶结点的最小键现在是5,所以前两个叶结点的父结点的键从7改为5。

下一步,假定我们删除键11。这个删除对第二个叶结点产生同样的影响;又一次把它的键数减少到低于最小数。不过,这次我们不能从第一个叶结点借键,因为后者的键数也到了最小数。另外,它的右边没有兄弟,也就无处可借。这样,我们需要合并第二个叶结点和它的兄弟,即第一个叶结点。

前两个叶结点剩下的三个键-指针对可以放在一个叶结点中,因此,我们把键5移到第一个叶结点并删除第二个叶结点。父结点中的键和指针需要进行调整,以反映它的子结点的新情况;具体地说,它的两个指针被换成一个指针(指向剩下的叶结点)且键5不再有用,也被删除。现在的情况如图4-28所示。

093039204.jpg

093041356.jpg



在图4-28中所看到的那样,该结点现在没有键且只剩一个指针。因此,我们试图从与它相邻的兄弟那里获得一个额外的键和指针。这一次,我们碰到容易的情况,因为根结点的另一个子结点可以提供它的最小键和一个指针。

变化如图4-29所示。指向键为13、17和19的叶结点的指针从根结点的第二个结点移到了它的第一个子结点。我们还修改了内部结点的一些键。键13原来位于根结点且表示通过那个被转移的指针可访问到的最小键,现在需要放到根结点的第一个子结点中。另一方面,键23原来用来区分根结点第二个子结点的第一个和第二个子结点,现在表示通过根结点第二个子结点可访问到最小键,因此它被放到根结点中。


7、B树的效率

B树使我们能实现记录的查找、插入和删除,而每个文件操作只需很少的磁盘I/O。首先我们注意到,如果每个块容纳的键数n相当大,比如10或更大,那么,分裂或合并块的情况将会很少。此外,这种操作必需时,绝大多数时候都被局限在叶结点,因此只有两个叶结点和它们的父结点受到影响。所以,我们基本上可以忽略B树重组的I/O开销。

然而,每次按给定查找键值查找记录都需要我们从根结点一直访问到叶结点以找到指向记录的指针。因为我们只读B树的块,所以磁盘I/O数将是B树的层数加上一次(对查找而言)或两次(对插入或删除而言)处理记录本身的磁盘I/O。我们肯定会这样问:B树到底有多少层?对于典型的键、指针和块大小来说,三层就足够了,除非数据库极大。因此,我们一般取3作为B树的层数。下面的例子说明了其原因。

例4.27 回忆一下我们在例4.19中的分析,我们当时确定每块可容纳示例数据的340个键-指针对。假若一般的块充满度介于最大和最小中间,即一般的块有255个指针。一个根结点,有255个子结点,有2552=65025个叶结点;在这些叶结点中,我们可以有2553,即约1.66×107个指向记录的指针。也就是说,记录数小于等于1.66×107的文件都可以被3层的B树容纳。

不过,对于每次查找,我们甚至可以通过B树用比3次还少的磁盘I/O来实现。B树根结点块是永久地缓存在主存中的绝佳选择。如果这样,那么每次查找3层的B树只需两次磁盘读操作。实际上,在某些情况下,把B树的第二层结点块保存在缓冲区中也是合理的。这样,B树的查找就减少到一次磁盘I/O再加上处理数据文件本身所需的磁盘I/O。

===============================我们是否该从B树中删除==================================

有一些B树的实现根本不对删除做修复。如果叶点键和指针太少,这种情况也允许保留。其基本理由在于,大多数文件的发展比较平衡,尽管有时可能出现使键数刚好少于最小数删除操作。但该叶结点可能很快增长并且再次达到键-指针对的最小数。

此外,如果记录有来自B树索引外的指针,那么,需要用“删除标记”来替换记录,并且我们不想用任何方式删除B树中的指针。在某些情况下,如果可以保证所有对删除记录的访问都将通过B树,我们甚至可以在B树叶结点中指向记录的指针处留下删除标记。这样,该记录的空间就可以重新使用。


oracle视频教程请关注:http://u.youku.com/user_video/id_UMzAzMjkxMjE2.html