1.B树索引(B-TREE)
B树索引是我们日常工作最最常用的索引,大家平时在工作中说的"索引"默认都是B树索引;
索引其实很简单,也很容易理解,用一本书的目录来形容最为贴切了,B树索引的结构跟图书馆的目录也很像。
2.B树索引的结构:
索引的顶层为根,它包括指向索引中下一层次的条目。下一层次为分支块,它又指向位于索引中下一层索引中下一层次的块,最底层的是叶节点,它包含指向表行的索引条目。叶块是双向关联的,这边与按键值升序或降序扫描索引;
3.索引叶条目的格式
一个索引条目包含以下组件:
条目头:存储列数和锁定信息
键列长度/值对:用于定义键中的列大小,后面跟随列值(此类长度/值对的数目就是索引中的最大列数)。
4.索引叶条目的特性
在非分区表的B 树索引中:
当多个行具有相同的键值时,如果不压缩索引,键值会出现重复
当某行包含的所有键列为NULL 时,该行没有对应的索引条目。因此,当WHERE 子句指定了NULL 时,将始终执行全表扫描。
5.对索引执行DML 操作的效果
对表执行DML 操作时,Oracle 服务器会维护所有索引。
下面说明对索引执行DML 命令产生的效果:
执行插入操作导致在相应块中插入索引条目。
删除一行只导致对索引条目进行逻辑删除。已删除行所占用的空间不可供后面新的叶条目使用。
更新键列导致对索引进行逻辑删除和插入。PCTFREE 设置对索引没有影响,但创建时除外。即使索引块的空间少于PCTFREE 指定的空间,也可以向索引块添加新条目。
B树是一个专门的多路树尤其适合在磁盘上使用而设计的。在B树的每一节点可包含大量的密钥。其每个节点也可以包含很多子树。 B树被设计在这众多的方向的分支出来,并含有大量的密钥的每个节点,使得树的高度相对较小。这就是说,只有节点的少数必须从磁盘读取检索块。其目的是获得对数据的快速存取,和与磁盘驱动器,这意味着读出一个非常小的数量的记录。需要注意的是一个大的节点大小(有很多的节点键)也与事实有一个磁盘驱动器可以经常读数据相当数量的一次配合。
用阶定义的B树
B 树又叫平衡多路查找树。一棵m阶的B 树 (注:切勿简单的认为一棵m阶的B树是m叉树,虽然存在四叉树,八叉树,KD树,及vp/R树/R*树/R+树/X树/M树/线段树/希尔伯特R树/优先R树等空间划分树,但与B树完全不等同)的特性如下:
树中每个结点最多含有m个孩子(m>=2);
除根结点和叶子结点外,其它每个结点至少有[ceil(m / 2)]个孩子(其中ceil(x)是一个取上限的函数);
若根结点不是叶子结点,则至少有2个孩子(特殊情况:没有孩子的根结点,即根结点为叶子结点,整棵树只有一个根节点);
所有叶子结点都出现在同一层,叶子结点不包含任何关键字信息(可以看做是外部接点或查询失败的接点,实际上这些结点不存在,指向这些结点的指针都为null);(读者反馈@冷岳:这里有错,叶子节点只是没有孩子和指向孩子的指针,这些节点也存在,也有元素。@研究者July:其实,关键是把什么当做叶子结点,因为如红黑树中,每一个NULL指针即当做叶子结点,只是没画出来而已)。
每个非终端结点中包含有n个关键字信息: (n,P0,K1,P1,K2,P2,......,Kn,Pn)。其中:
a) Ki (i=1...n)为关键字,且关键字按顺序升序排序K(i-1)< Ki。
b) Pi为指向子树根的接点,且指针P(i-1)指向子树种所有结点的关键字均小于Ki,但都大于K(i-1)。
c) 关键字的个数n必须满足: [ceil(m/ 2)-1]<= n <= m-1。
一棵m阶的B树满足下列条件:
1.每个结点至多有m棵子树;
2.除根节点外,其它每个分支结点至少有⌈m/2⌉棵子树;
3.根节点至少有两棵子树(除非B树只包含一个结点);
4.所有叶节点在同一层上。B树的叶节点可以看成一种外部结点,不包含任何信息;
5.有j个孩子的非叶节点恰好有j-1个关键码,关键码按递增次序排列。
用度定义的B树
针对上面的5点,再阐述下:B树中每一个结点能包含的关键字(如之前上面的D H和Q T X)数有一个上界和下界。这个下界可以用一个称作B树的最小度数(算法导论中文版上译作度数,最小度数即内节点中节点最小孩子数目)m(m>=2)表示。
每个非根的内结点至多有m个子女,每个非根的结点必须至少含有m-1个关键字,如果树是非空的,则根结点至少包含一个关键字;
每个结点可包含至多2m-1个关键字。所以一个内结点至多可有2m个子女。如果一个结点恰好有2m-1个关键字,我们就说这个结点是满的(而稍后介绍的B*树作为B树的一种常用变形,B*树中要求每个内结点至少为2/3满,而不是像这里的B树所要求的至少半满);
当关键字数m=2(t=2的意思是,mmin=2,m可以>=2)时的B树是最简单的(有很多人会因此误认为B树就是二叉查找树,但二叉查找树就是二叉查找树,B树就是B树,B树是一棵含有m(m>=2)个关键字的平衡多路查找树),此时,每个内结点可能因此而含有2个、3个或4个子女,亦即一棵2-3-4树,然而在实际中,通常采用大得多的t值。
B树中的每个结点根据实际情况可以包含大量的关键字信息和分支(当然是不能超过磁盘块的大小,根据磁盘驱动(disk drives)的不同,一般块的大小在1k~4k左右);这样树的深度降低了,这就意味着查找一个元素只要很少结点从外存磁盘中读入内存,很快访问到要查找的数据。
m阶多路树是一个有序的树,其中每个节点最多有m个子节点。对于每个节点,如果k是子节点中的实际数目,则节点的密钥数是k - 1。如果键和子树被安排在一个搜索树的方式,那么这就是所谓m的顺序多路搜索树。例如,以下是顺序4.注意的一个多路搜索树中的每个节点的第一行显示的钥匙,而第二行示出了指针的子节点。当然,在任何有用的应用将有与每个键相关联的数据的记录,以便在每一个节点的第一行可能的记录数组,其中每个记录都包含一个键及其关联的数据。另一种方法是将有每个节点的第一行包含的记录的阵列,其中每个记录都包含了关联的数据记录,它是在另一文件中找到一个键和一个记录编号。当数据记录大该最后方法经常被使用。这个例子将使用第一种方法。
这就是说,在键和子树“排列在搜索树的前面”?假设我们定义节点如下:
typedef struct
{
int Count; // number of keys stored in the current node
ItemType Key[3]; // array to hold the 3 keys
long Branch[4]; // array of fake pointers (record numbers)
} NodeType;
然后,4阶多路搜索树必须满足相关的键的顺序如下条件:
在每个节点上的键是按升序排列。
在每一个给定的节点(称之为节点)符合下列条件:
开始记录Node.Branch子树[0]只有不到Node.Key键[0]。
起始于记录Node.Branch子树[1]仅具有比Node.Key更大按键[0],并在同一时间小于Node.Key[1]。
起始于记录Node.Branch子树[2]仅具有比Node.Key更大的键[1],并在同一时间小于Node.Key[2]。
起始于记录Node.Branch子树[3]仅具有那些比Node.Key更大键[2]。
请注意,如果小于全数字键是在Node,这4个条件被截断,使他们说话键和分支的适当数目。
这在推广到具有多路与其它顺序搜索树明显的方式。
m阶B树是m阶,使得多路搜索树:
所有叶结点都在底部;
所有内部节点(或许除了根节点)具有至少CEIL(M / 2)(非空)的子节点;
根节点可以具有少至2个子节点,如果它是一个内部节点,并能明显地有没有子节点如果根节点是叶(即,整个树仅由根节点);
每个叶节点(比如果它是叶根节点以外)必须包含至少CEIL(M / 2) - 1密钥。
注意,CEIL(x)是所谓的整函数。它的值是大于或等于x的最小整数。从而CEIL(3)= 3,CEIL(3.35)= 4,CEIL(1.98)= 2,CEIL(5.01)= 6,CEIL(7)=7等
B树是凭借的事实,所有的叶节点都必须在底部相当均衡的树。条件(2)试图保持树通过坚持每个节点至少有一半是子节点的最大数目相当浓密。这将导致树“扇出”,让从根到叶的路径很短,即使在包含大量数据的树。
以下是有5种顺序的B树例子,这意味着(其他根节点)所有的内部节点具有至少CEIL(5/2)= CEIL(2.5)= 3的子节点(并因此至少2个键)。当然,一个节点可以有子节点的最大数量是5(钥匙的最大数量为4)。根据条件4中,每个叶节点必须包含至少2密钥。在实践中B树的顺序远远大于5。
问:你如何在上面的树来查找的搜索S和J?你会如何做一个排序的“按顺序”遍历,即,将产生按升序排列的字母遍历? (一直做这样的遍历是低效的,因为这将需要大量的磁盘活动,因此会很慢!)
插入一个新块
据克鲁斯(查看下面的引用)的插入算法过程如下:当插入一个块,首先做一个搜索它在B树。如果该块是不是已经在B树,这个不成功的搜索将在一个叶结点结束。如果在该叶节点中仍存在空节点,那么在此处插入新块。注意,这可能需要一些现有键来移动一个到右侧,以便划分出空间给新的块。相反,如果这个叶节点是满,使得没有房间添加新的项,则该节点必须是“分裂”约一半的按键进入一个新的节点以这一个的右边。中值(中)键被向上移动到父节点。(当然,如果该节点没有余地,则它可能不得不被分割为好。)请注意,添加到内部节点时,不仅可能,我们有一个位置移动某些键的右侧,但相关的指针必须被向右移动为好。如果根节点是不断分裂,中位键向上移动到一个新的根节点,从而导致树由一个在高度增加。
让我们的方式通过类似于由克鲁斯给出一个例子。将下面的字母插入,变成顺序为5的空B树:C N G A H E K Q M F W L T Z D P R X Y S的三维p - [R X Y的订单5是指节点最多可有5个孩子和4个按键。比根其他所有节点都必须有一个最低2键。前4个字母被插入到相同的节点,从而导致这样的结果:
当我们尝试插入H,我们发现在这个节点没有空间,所以我们将其分成2个节点,移动平均商品G成一个新的根节点。请注意,在实践中,我们刚刚离开A和C在当前节点,并将H和N插入一个新的节点,位于原节点的右侧。
插入E,K和Q的操作可以继续进行,而不需要任何分裂:
插入M需要一个分裂。另外,M恰好是中位数键等被向上移动到父节点。
然后无需任何分裂加入字母F,W,L和T。
当加入Z为,最右边的叶结点必须被分割。中间块T被向上移动到父节点。注意,通过向上移动平均键,树被保持相当平衡,每个所得到2键的结点。
D的插入引起的最左边的叶被分裂。 D恰好是中位数键等是一个向上移动到父节点。字母P,R,X和Y是那么无需任何分裂加入:
最后,S被插入的时候,用N,P,Q和R分裂节点,发送位数Q上到父节点。然而,在父节点是满的,所以它分割,发送位数m,最高以形成一个新的根的节点。注意,如何从旧的父节点入住3指针在包含D和G修订后的节点。
删除块
当我们在最后一部分离开时,删除H。当然,我们首先进行查找,以找到H.由于H是在叶和叶片具有比键的最小数目越多,这很容易。我们搬过来且H已经在K与L以上,其中在K过。这表明:
接下来,删除T.由于T不是一个叶子,我们发现它的后继者(按升序排列的下一个块),这恰好是W,移动W取代T.这样,我们真的要做的是从叶结点,我们已经知道该怎么做,因为这片叶子都有额外的键删除W的在所有情况下,我们减少缺失到的缺失在一片叶结点,通过使用此方法。
接着,删除R.虽然R是在叶结点,此叶结点不具有额外的键;删除结果中的一个节点只有一个键,如果同级节点眼前的左这是不是顺序为5的B树接受或右侧有一个额外的按键,我们可以借用父节点和移动兄弟结点。在我们的具体情况下,兄弟姐妹的权利有一个额外的键。因此,继承W¯¯S(在发生删除的节点的最后一个键)的,是从父结点向下移动,并且在X向上移动。(当然,在S上移动,使得W能够在适当的位置被插入。)
最后,让我们删除E这一次会导致很多问题。虽然E是在叶结点,叶子已经没有多余的键,也没有兄弟姐妹眼前的左边或右边。在这种情况下,叶结点可以与这些两兄妹之一进行组合。这包括向下移动,这是与这些两叶结点之间的父结点的关键。在我们的例子,让我们结合含F用含有C叶结点我们也向下移动D。
当然,你立即看到父节点现在只包含一个键,G这是不能接受的。如果这个问题节点有一个兄弟到其直接向左或向右的有一把备用钥匙,那么我们将再次“借”的关键。假设为正确的兄弟姐妹(与QX的节点)在它有一个更重要的某处Q右侧然后,我们就设法使得m下降到节点的子节点和移动Q上。然而,Q的旧左子树随后将不得不成为M的右子树,换言之,N个P个节点,将通过该指针字段被附加到M的新的位置的右侧。因为在我们的例子中,我们也没有办法借用同级的关键,我们必须再次与兄弟结点相结合,并从父结点下移。在这种情况下,树的高度由一个收缩。
一个例子
这里是一个5中不同顺序的B树,让我们试着从中删除C结点。
我们首先找到直接后继,这将移动D更换C.,太多的结点需要移动将使得我们的工作变得很难。
由于既没有兄弟姐妹包含E中的节点的左侧或右侧有一个额外的键,我们必须节点与这些两兄妹中的一个结合。让我们从A B节点巩固。
但现在含F节点没有足够的密钥。然而,它的兄弟姐妹有一个额外的键。因此,我们从兄弟借M结点,移动到父结点,并带来J结点加入F结点注意,在K结点和L结点被重新连接到J结点的右侧。
另一个B-Tree的例子
下面是一个完全编码的例子。有两种方案:btmake创建一个B树表,btread允许用户从B树查表(读取)块。在本实施例中,每个键是一个字和相关联的数据是字的定义。编码细节相当复杂。
itemtype.h
table.h(设置了一个抽象基类表)
btree.h(派生的B树表类)
btree.cpp
btree.txt(数据使用的文本文件)
btmake.cpp(创建一个B树表)
btread.cpp(从B树表中读取数据)
在btree.h你会发现,我们正在建立一个12顺序的B树。该B树的每个节点是一个包含键数组(实际上密钥和相关的数据),子节点指针数组(记录记录号),以及有多少密钥实际上被存储在该节点的计数。需要后者由于节点可能不完全充满密钥。
在同一个文件你会发现BTTableClass类的声明。请注意,有列出四个数据字段:根节点的记录数的多少个节点都在B树的计数,每个节点(每当我们做输入节点的/输出需要的话)的字节数,我们正在使用的整个当前节点。这最后一个领域给了我们一个方便的地方放置,我们在任何给定点工作的节点的数据。请记住,这个类的对象从抽象基类继承三个数据字段:文件流,物品(包括字和它的定义的)中的表的数量,以及指示如果我们在打开表一字符读或写模式。你可以看到许多的这些目的和在一个单独的绘图其相关联的数据文件中的一个的细节。如果你愿意,使用该图的放大版本。
注意如何DEBUG的定义可以在btree.h被注释了。如果你想运行已经包含调试代码确保它是注释。再看功能转储,检查和CheckSubtree进一步的细节。在btmake.cpp笔记如何加载功能检查,看看是否DEBUG已经被定义,如果是,执行某些调试代码。即使在btree.cpp还有一个就是取决于是否不DEBUG被定义有条件地整理了一些调试代码。在这里,程序结束,并在节目中打印的关键点一个字母为正在执行什么操作的迹象时,调试代码打印整个B树的转储。是字母和它们的含义如下:
的R - 从文件执行读
W - 做一个写文件
正在执行下推 - P
我 - 正在执行插入
正在执行分割 - S
没有尝试在这里做出了解释这一切示例程序的细节,因为它是相当费时耗力。B树的检索功能的详细解释,以说明一些这一计划的运作是可用的。如果需要,读者可以更仔细地检查等功能。大部分的功能其实是相当有下推功能,这是相当复杂的异常简单。
根据谢弗(参阅参考资料)的变化就可以了,B树和大型商业数据库通常用于提供快速存取的数据。事实上,他说,他们是“为需要插入,删除,和主要范围搜索应用程序的标准文件组织”。称为B +树变体是常规的。另一种变型是B *树,这是非常相似的B +树,但试图保持节点约三分之二充分最小。
在B +树,数据记录只存储在树叶。内部节点只存储密钥。这些键用于指示一个搜索到适当的叶。如果一个目标密钥小于在内部节点的密钥,然后只在其左侧是跟着指针。如果一个目标密钥是大于或等于在所述内部节点的密钥,然后只在其右侧后面的指针。叶片也连接在一起,使得所有的B +树的键可以以升序遍历,只需通过沿树的底部水平此链表通过所有节点去。
当一个B +树在磁盘上实现的,它是可能的叶含有键,指针对,其中该指针字段指向与该键相关联的数据的记录。这使得数据文件以从B +树,其功能是作为一个“索引”给予的排序在数据文件中的数据分别存在。这是怎样的B +树在数据库中被使用。当然,指针是创纪录的数字,我们在磁盘上创建动态数据结构时使用的典型假指针。请注意,此B +树索引方案允许一个数据文件到具有几个这样的指标,各由一个不同的密钥字段给出的排序。
作为一个例子,考虑一个B +树的顺序200,其叶子可以各自含有多达199键(约200)。让我们假设根节点具有至少100名子节点(虽然我们知道它被允许至少有两个)。 A2级的B +树,以满足这些假设可以存储多达约10,000的记录,因为至少有100叶结点,每片含至少99键(约100)。这种类型的A3级B +树,最多可存储约100万项。 A4级B +树,最多可存储约100项。为了得到提高数据访问速度,根节点通常保存在主内存。甚至根的子节点可以容纳在主存储器中。因此,人们可以发现,只有2或3磁盘读取亿关键之一。如果,由于是共同的,相关联的数据记录存储在一个单独的文件中,有一个额外的读得到与键有关的数据。此外,请注意,如果根节点有较少,我们假设100个子节点,这会进一步减慢查找。
B树算法很实用,但自己初次翻译,感受颇深,写的不明之处,欢迎交流讨论。
Oracle索引的使用使得我们的操作简便快捷,相信大家深有体会。
算法,深入思考,可以让人变得聪明哦!共勉!
参考:
CIS-Btree
B树索引学习总结
浅谈算法和数据结构(10):平衡查找树之B树
B树索引和位图索引的结构介绍
CSDN博客B-Tree索引