1.3 B-tree索引的物理结构
B-tree索引的物理结构,也即索引是如何在数据块中存储的。因此,我们就要了解,根块,分支块和叶子块中的内容都有什么。而为了可以观察到这一结构,我们需要借助和使用Oracle提供的数据块DUMP方法,将指定的数据块内容,输出到跟踪文件中。我们从跟踪文件中观察和了解数据块的内容和结构,进而了解索引的物理结构。
1.3.1 观察索引数据块和索引树形结构的基本方法
1、DUMP数据块的方法
Oracle提供了DUMP数据块的方法:
Alter system dump datafile <n> block <m>;
其中
<n>表示目标数据块所在的数据文件号;
<m>表示目标数据块的块号。
2、查看B-tree索引树型结构的方法
Oracle提供了通过以下方法,输出目标索引树形结构的方法:
alter session set events ‘immediate trace name treedump level <x>’;
其中<x>表示目标索引的OBJECT_ID。
以上两个方法,均是将相应的内容输出到跟踪文件,因此,我们还要知道跟踪文件的位置和名称,才能打开并查看其中的内容。
对于11g及之后版本的Oracle,可以在执行上述命令的会话中运行以下查询来获取跟踪文件的位置和名称:
select value from v$diag_info where name='Default Trace File';
对于11g之前版本的Oracle,可以在执行上述命令的会话中运行以下查询来获取跟踪文件的位置和名称:
select d.value
|| '/'
|| LOWER (RTRIM(i.INSTANCE, CHR(0)))
|| '_ora_'
|| p.spid
|| '.trc' trace_file_name
from (select p.spid
from v$mystat m,v$session s, v$process p
where m.statistic#=1 and s.sid=m.sid and p.addr=s.paddr) p,
(select t.INSTANCE
FROM v$thread t,v$parameter v
WHERE v.name='thread'
AND(v.VALUE=0 OR t.thread#=to_number(v.value))) i,
(select value
from v$parameter
where name='user_dump_dest') d;
下面,我们就使用上面的方法,来观察和了解一下索引的树形结构,以及索引的根块,分支块和叶子块中的内容。
首先,我们创建一个只有一个索引条目的索引,显然,这将会是根块,分支块和叶子块在同一个块上的索引。
图 2如图2所示,我们创建了一个名为index_test的表,该表中只有1列,名为ID列,数据类型为VARCHAR2(200)。同时,在该列上创建了一个名为ind_test_id的索引。然后,向该表中插入一行记录,其ID列中的内容为’50’打头的字符后面,跟了198个字符’0’,总共200个字符。
为了查看索引ind_test_id的树形结构,我们需要先查到该索引的object_id,并使用该object_id代入获取索引树形结构的命令中:
图 3此时,索引树形结构的信息已经输出到当前会话的跟踪文件中。由于这里用于演示的环境为11.2.0.4的版本,所以,我们可以使用如下方法来获取当前会话的跟踪文件位置和名称:
图 4
打开图4中所示的文件,可以发现有如下内容:
图 5
从图5中可以看到,此时,在“begin tree dump”和“end tree dump”之间,只有一行记录,这里的一行,对应着索引中的一个数据块。且该行有“leaf”字样,表示这个数据块是叶子块。由于该索引中只有这一行,所以,此时,该数据块也是根块和分支块。
随后的“ 0x100027b 16777851”是该数据块的dba(data block address)用十六进制和10进制分别表示的内容,即数据块的地址。其内容是由该数据块所在文件号和数据块号共同构成的。
再往后的“(0: nrow: 1 rrow: 1)”中各部分的含义如下:
0: 表示该索引块,在本层(根、分支和叶子层)的序号。根层从0开始编号,分支层和叶子层从-1开始编号。
nrow:1 表示该索引块中一共有1个索引条目。此数值包括被标记为删除的索引条目。
rrow:1 表示该索引块中实际有1个索引条目。此数值不包括被标记为删除的索引条目。
通过DBMS_UTILITY.DATA_BLOCK_ADDRESS_FILE和DBMS_UTILITY.DATA_BLOCK_ADDRESS_BLOCK可以获取到对应的文件号和块号(注:提供的地址值应该是十进制表示的。如果提供十六进制的值,则需要用to_number做向十进制转换的处理)。
图 6
如上图所示,我们通过相应的查询,获取到该索引块位于4号数据文件的635号块。
接下来,我们使用前述介绍的DUMP数据块的方法,来查看该索引块中的内容:
图 7
如上图所示,由于执行DUMP数据块的用户,需要有DBA的角色,所以,我们这里直接切换到了SYS用户下来执行。
查看生成的跟踪文件的内容,由如下几部分内容构成:
图 8
索引块的第一部分块头的相关信息。对于11g及之后的版本,这里会有“Block dump from cache:”之下,直至“Block dump from disk:”及之上的内容。表示这部分信息,是从DB BUFFER CACHE中获取到的。显然,“Block dump from disk:”之下的内容,就表示是从磁盘中获取的。我们知道,当我们修改数据块中的内容后,无论是否提交,其只是把变化记录到REDO中,磁盘中对应的数据块并不一定会立即得到更新。所以,我们在实验时,为了稳妥起见,在DUMP数据块之前,需要执行以下命令(请勿在生产环境中随意使用):
alter system checkpoint;
图 9
接下来,会有类似上图所示的内容,这是数据块中的内容,用十六进制的方式进行显示。这里的每一行,表示16个字节。每行最前边以“7F2FF29E”打头的一串字符,是本行首字节的内存地址。而中间出现的“Repeat xxx times”字样的内容,表示上面这一行的内容,在此处又重复出现了xxx次。
再往后,会有下图所示的类似内容:
图 10
这里显示的是索引块中ITL槽的信息等。
图 11
如上所示,这里的以“kdx”打头的信息,其主要含义如下:
Kdxcolev:该索引块的级别号。0表示叶子块。
Kdxcolok:该索引块上是否持有锁。0表示没有锁。
Kdxconco:该索引由几列构成(ROWID也算1列)。因此,2表示该索引是由1个表列和1个ROWID列构成的。
Kdxconro:该索引块中包含有多少个索引条目。
Kdxcofbo:该索引块中可用空间的起始位置。
Kdxcofeo:该索引块中可用空间的结束位置。需要注意的是,由于索引块和表块一样,也是堆(heap)块,因此,新插入的索引条目,并不是从可用空间的起始位置处开始占用,而是由底向上堆放,所以,会从可用空间的结束位置向起始位置方向占用。
Kdxcoavs:该索引块中可用空间的大小(用十进制表示),该值等于Kdxcofeo- Kdxcofbo。
Kdxlenxt:后一个叶子块的地址
Kdxleprv:前一个叶子块的地址
Kdxledsz:ROWID信息占用的字节数。非唯一索引,此值为0。
Kdxlebksz:该索引块初始化时的可用空间大小(用十进制表示)。
图 12
最后一部分,是索引条目的信息。其中:
row#0:表示第一个索引条目。从0开始编号,而该编号是按照索引列中值的顺序排序的。如果未来在该索引块中,插入了比该值小的索引条目,则最小的索引条目的编号为0。
[7821]:索引条目的偏移量(使用十进制表示)
len=:表示该索引条目占用的字节数(含该索引条目中ROWID占用的字节数)
col 0:索引中第1列的值(使用十六进制表示)。在本示例中,字符’5’所对应的ASCII码为十六进制的35,而字符’0’所对应的ASCII码为十六进制的30。
col 1:索引中第2列的值(使用十六进制表示)。在本示例中,该列是ROWID信息。通常情况下,ROWID由10个字节表示。其中前4个字节,表示索引所在基表对象的data_object_id。对于非分区表,或分区表中的本地分区索引,索引中条目都是指向同一个段对象的,因此,其对应基表(或分区)的data_object_id是可以通过数据字典查询到的,所以,其并没有存储前4个字节。但对于全局分区索引,由于其索引条目指向的基表中的行,可能分布在多个段(分区)上,即可能涉及多个data_object_id,所以,在索引条目中,会使用完整的10个字节来保存ROWID信息。
我们向表中继续插入记录,进而不断向索引中继续插入索引条目。使其变成高度为2的索引。根据前边我们观察到的内容,我们可以看到在本例中,单个索引条目占用的空间为211个字节,索引块的初始可用空间为8032个字节,因此,粗略计算一下,在只有一个索引块的情况上,大致可以容纳下38个索引条目。因此,我们向表中继续插入40行,使该索引变为一个2层的索引。执行如下插入语句:
图 13
此时,查看该索引的树形结构,以验证其是否已成为2层高度的索引。执行如下SQL:
图 14
查看上图中跟踪文件的内容,如下图所示:
图 15
如上图所示,我们可以看到这时一共三行记录,即此时索引是由三个索引块组成。其中有一个分支块(即根块)和两个叶子块。
其中分支块对应的行,有“branch”字样。“(0: nrow: 2, level: 1)”的含义与前边的介绍类似,差异在于,这里没有rrow,而是变成了level。其含义是该索引块所在的层级为1(叶子层为0级)。
而叶子块对应的两行,其各部分的含义,在前边已经有所介绍,这里要着重说明的是,括号内的第1个数字,分支层和叶子层从-1开始编号,根块是从0开始编号。
1.3.2 叶子块的结构
1.3.2.1 非唯一索引的叶子块结构
同前,我们使用DBMS_UTILITY.DATA_BLOCK_ADDRESS_FILE和DBMS_UTILITY.DATA_BLOCK_ADDRESS_BLOCK获取到编号为-1的叶子块对应的文件号和块号后,使用alter system dump datafile n block m的方法,将该索引块的内容DUMP到跟踪文件中查看。
图 16
如上图所示,这里我们可以看到与此前整个索引只有一个索引块时相比,最重要的一个区别就是kdxlenxt有了实际值,而不再是0。这里的16777852,就是十进制表示的数据块地址dba(data block address)。而且,我们再回看一下该索引的树形结构图(图15)中的信息,我们可以看到,16777852正是第二个叶子块的dba。
同理,我们也可以推测出,第二个叶子块的Kdxleprv的值,应该是16777855。经过实际验证,也确实如此。如下图所示:
图 17
回到第一个叶子块DUMP出的内容,我们查看记录有索引条目实际值的部分:
图 18
可以看到此时其最大的row#是21,由于row#是从0开始编号,所以,该叶子块中共存有22个索引条目。这个值,与索引树形结构(图15)中看到的信息也是一致的。同样,第二个索引叶子块中的最大row#应该是18。经过实际验证,也确实如此。如下图所示:
图 19
1.3.2.2 唯一索引的叶子块结构
前面我们提到过,如果是唯一索引,叶子块的存储的内容和结构会有一些变化。下面通过展示一个唯一索引叶子块的DUMP内容,来演示一下这种变化:
图 20
如上图所示,我们可以看到主要区别是:
1、 kdxledsz处的值为6,而不是此前的0。
2、 索引条目内容部分,其row#对应行的最后,多了类似“data:(6): 01 00 03 0b 00 02”的内容,这个内容就是ROWID的后6个字节。对于非唯一索引,这个信息是出现在最后一个索引列中。即此前示例中的col 1中。
3、 相应的,表示索引列数量的kdxconco也显示只有1列了,而不是非唯一索引时的2列。
1.3.3 索引根块的结构
我们使用前面介绍的方法,获取到图15中所示的索引的根块所在的文件号和块号后,将其内容DUMP出来后,观察到的结果与最初索引只有一个索引块时的主要差异如下:
图 21
如上图所示,我们可以看到kdxcolev的值由0变为了1。这是因为0是叶子块层,而该索引块是叶子块层的上一级,所以,其值为1。若索引高度为3,即在根块和叶子块层之间,还有一层分支层的话,则分支层的索引块的该值为1,而根块的该值将变为2。
其次,我们看到多了一个kdxbrlmc,表示指向下一层最左侧索引块的地址。
再次,我们看到索引条目信息部分,只有一个row#0。其col 0 只占用了1个字节,其值为十六进制的33,表示字符’3’。而我们知道,该索引的ID列上的值,应该有200个字符,而不是1个字符。这是因为分支块(包括根块)的主要作用,就是为要寻找的键值,指向其在下一层的位置。所以,它没有必要存储完整的键值,只要其保存的值,足够识别下一层所存储的键值的起始范围即可。我们可以看到row#0所指向下一层的索引块的地址(dba)为16777852。我们查看该索引块的最小值,即该块中的row#0的值。如下图所示:
图 22
我们可以看到该叶子块中的最小值就是字符’3’开始的,后面跟有199个字符’0’的键值。所以,从索引根块的物理结构中,我们可以看到,索引分支块(包括根块)是通过记录位于其下层叶子块(或分支块)的最小值,如果条件允许,甚至只记录最小值的部分值,来实现区分并指向下一层索引块的。其并不是像在索引逻辑结构中展示的,记录的是下一层分支块的值的范围的。
继续回到图21,我们还可以看到col 1后面的值为“TERM”,其含义就是表明col 1中记录的值并非完整值。
综上,我们可看到,虽然根块指向了两个叶子块,但根块的索引条目区,只有1个索引条目,指向的是第二个叶子块。而指向第一个叶子块的地址,是存储在块头的kdxbrlmc中的。我们前面提到过,B-tree索引中同一层的索引块是有序的。所以,存储在第一个叶子块中的最大值,一定小于等于第二个叶子块中的最小值。而当我们对一个索引列上求MIN()时,正是利用这一点,从根块,逐级访问最左侧的索引块,直至到达最左侧的叶子块,从中取出row#0的记录值即可。
1.3.4 索引分支块的结构
为了构建一个高度为3的索引,即有根块,分支块和叶子块构成的索引,显然,我们要在前边2层索引的基础上,让根块中存储不下索引条目,导致必须至少要用两个索引块来存。又由于B-tree索引有且只有一个根块,所以,必然逻辑上,会在这两个块之上,存在一个根块。这时,2层索引就会变成3层的索引。
但是,从前边的实验中,我们也观察到(图21),根块中的一个索引条目,只占用了很少的字节数。这意味着,对于一个还有8019个字节可用空间的根块而言,会需要数百甚至上千个索引条目,才会出现我们期望的情况。而根块中有多少索引条目,就意味着,要有同样数量的叶子块;而每个叶子块我们前边大致计算过,可以大约存储40个索引条目,即大约40行表记录对应的值。也就是说,我们可能要构建一个上万行的这样的表,才有可能生成一个3层的索引。
因此,我们决定换一种ID列值的构建方法。以前,是后补字符’0’,现在,我们改为前补字符’0’。其目的是让在索引的根块中保存指向下层叶子块的键值时,为了区分不同的叶子块中的最小值,其就需要保存尽可能完整的字符串,从而占用更多的字节,降低了根块中可容纳的索引条目的数量。即通过插入较小的行,就可以实现3层索引的目标。
图 23
查看此时索引的树形结构信息,如下图所示(为减少篇幅,只对前面部分做了截图):
图 24
如上图所示,我们可以看到至少出现了三行有“branch”字样内容,且其中一行branch的缩进更靠左,同时,其level值也与另两行branch不同,其level值为2,这说明该索引已经是3层了。
现在,我们对“branch: 0x10002c7 16777927 (0: nrow: 25, level: 1)”这个分支块进行DUMP处理,观察其结构。
图 25
如上图所示,我们可以看到,在该分支块的索引条目部分,其col 0部分,已经要占用199个字节了,而不是此前看到的,只需要1个字节即可了。而且,从col 1部分的“TERM”也可以知道,这199个字节也是部分键值(完整键值应该是200个字节)。从上图中col 0中的具体内容来看,该指向的叶子块中存储的最小键值就是以197个字符’0’打头,后跟字符’42’的值。我们来验证一下,看看对应的叶子块(dba: 16777881)中的最小值,是否符合这个特征。
对该叶子块DUMP后,观察其中的row#0中的值,其结果如下:
图 26
如上所示,该叶子块中的最小值为是以197个字符’0’打头,后跟字符’420’的值。其完全符合我们的预期。
通过对上述知识的了解和掌握,下面,我们将进入到我们本部分内容的重点:深入理解B-tree索引的访问方法以及我们对索引的认识上可能存在的几个误区。