索引的访问方法之唯一扫描
2.1 跟踪索引的访问方法
如何跟踪索引在使用过程中,是如何实现的索引的唯一扫描,范围扫描、快速全扫描,全扫描和跳跃扫描的呢?这里,我们需要借助Oracle提供的一个专门用于跟踪一致性读(逻辑读)的内部事件—10200 event。
该事件可以通过在会话级别设置ALTER SESSION SET EVENTS,来跟踪当前会话中,在哪些数据块上发生了一致性读,以及发生的次序。下面,我们就尝试使用这个内部事件,来分析一下访问索引时,其会按什么样的顺序来访问哪些索引块。
2.2 索引唯一扫描
索引唯一扫描(INDEX UNIQUE SCAN)一定发生在唯一索引之上,而且,施加在唯一索引列上的逻辑比较操作符必定是相等比较,因此,通过索引唯一扫描最多只会有一个索引条目满足特定的谓词。如果是在唯一索引列上施加了非相等比较,比如是>=、>、<=、<这样的比较操作符,即便最终只有一个索引条目满足,但其采用的访问方法,也不是索引唯一扫描。
下面,我们构建一个测试表和测试索引,来观察索引唯一扫描的行为。
图 27
如上所示,我们构建了一个名为tab_unique的表,其中只有两列。其中c1列定义了一个长度为500的char类型。之所以定义的长度较大,是希望在较少行数的情况下,就可以构造出一个3层高度的索引。
然后,我们在c1列上创建了一个唯一索引,如下所示:
图 28
运行以下查询,我们来构造出索引唯一扫描的行为。
图 29
如上所示,我们从执行计划中看到对索引ind_tab_unique_c1采用的是索引唯一扫描(INDEX UNIQUE SCAN) 的访问方法。查询的结果也只有一行。
但是,如果在c1列使用非等于的比较操作符,即使最多也只返回1行记录,但此时对索引的访问方法,也不会是索引唯一扫描的访问方法。如下所示:
图 30
如上图所示,我们的查询SQL使用了非相等的比较操作符,虽然返回的结果也只有一行,但此时对索引的访问方法是索引范围扫描(INDEX RANGE SCAN)。
下面,我们就用event 10200来跟踪一下索引唯一扫描的行为,观察一下索引唯一扫描是如何在索引上访问相应的索引块的。为了规避回表操作对结果的影响,我们将上面的查询略微调整了一下,返回的列由rn,改为c1。这样一来,只需要访问索上就可以获取到相应的列,而不需要回表去获取rn列上的值了。
(注,为了避免因目标表上无统计信息时,数据库会使用动态采样的方式来即时获取目标表上的统计信息,从而导致相关动作产生的一致性逻辑读影响跟踪的结果,建议在进行操作前,收集目标表的统计信息,以避免这种情况的发生。)
图 31
获取跟踪文件的位置和名称:
图 32
查看跟踪文件中的内容,我们可以看到类似如下的内容:
ktrgtc2(): started for block <0x0007 : 0x01801e43> objd: 0x00017863
env [0x7fd80a05ab0c]: (scn: 0x0000.007a20c2 xid: 0x0009.01f.0000155c uba: 0x00c002e8.0935.01 statement num=0 parent xid: 0x0000.000.00000000 st-scn: 0x0000.007a207f hi-scn: 0x0000.00000000 ma-scn: 0x0000.007a0f80 flg: 0x00000661)
ktrexc(): returning 2 on: 0xc0db208 cr-scn: 0xffff.ffffffff xid: 0x0000.000.00000000 uba: 0x00000000.0000.00 cl-scn: 0xffff.ffffffff sfl: 0
ktrgtc2(): completed for block <0x0007 : 0x01801e43> objd: 0x00017863
ktrgtc2(): started for block <0x0007 : 0x01801e56> objd: 0x00017863
env [0x7fd80a05ab0c]: (scn: 0x0000.007a20c2 xid: 0x0009.01f.0000155c uba: 0x00c002e8.0935.01 statement num=0 parent xid: 0x0000.000.00000000 st-scn: 0x0000.007a207f hi-scn: 0x0000.007a20c2 ma-scn: 0x0000.007a0f80 flg: 0x00000662)
ktrexc(): returning 2 on: 0xc0db208 cr-scn: 0xffff.ffffffff xid: 0x0000.000.00000000 uba: 0x00000000.0000.00 cl-scn: 0xffff.ffffffff sfl: 0
ktrgtc2(): completed for block <0x0007 : 0x01801e56> objd: 0x00017863
ktrgtc2(): started for block <0x0007 : 0x01801e4b> objd: 0x00017863
env [0x7fd80a05ab0c]: (scn: 0x0000.007a20c2 xid: 0x0009.01f.0000155c uba: 0x00c002e8.0935.01 statement num=0 parent xid: 0x0000.000.00000000 st-scn: 0x0000.007a207f hi-scn: 0x0000.007a20c2 ma-scn: 0x0000.007a0f80 flg: 0x00000662)
ktrexc(): returning 2 on: 0xc0db208 cr-scn: 0xffff.ffffffff xid: 0x0000.000.00000000 uba: 0x00000000.0000.00 cl-scn: 0xffff.ffffffff sfl: 0
ktrgtc2(): completed for block <0x0007 : 0x01801e4b> objd: 0x00017863
由于我们只需要关注访问了哪些块,以及访问顺序,所以,我们只需要关注有“ktrgtc2”字样的行。
图 33
如上所示,我们通过过滤“ktrgtc2”字样,一共获取到了6行输出。而且,我们发现他们总是以“started for block”和“completed for block”成对出现的。而block 后面尖括号中的内容,表示的是数据块所在的表空间和该数据块的dba地址。比如“started for block <0x0007 : 0x01801e43>”中的“0x0007”表示是v$tablespace视图中,ts#为7的表空间;“0x01801e43”表示的是所访问的数据块的dba地址。而“objd: 0x00017863”表示的是该数据块所属对象(比如索引,表等)的data_object_id。
这成对出现的行输出,差异只在一行是“started”,一行是“completed”,所以,我们还可以进一步简化,只取“started”这一行,这样,从跟踪文件中查看内容时,会更简洁。
图 34
如上所示,我们可以看到,使用10200 event跟踪期间,访问了三个数据块。这三个数据块,均属于同一个对象,该对象的data_object_id是0x00017863。我们来查询一下该对象:
图 35
如上所示,我们可以看到,该对象是我们示例中创建的唯一索引ind_tab_unique_c1。
下面,我们再来看一下这里访问的三个数据块,都是这个索引中的哪些块呢?我们先查看一下该索引的树形结构信息(请参考前面介绍的获取索引树形结构的方法)。如下所示(被访问的三个数据块已用红框标识):
图 36
如上图所示,这是一个3层的索引,有一个根块“branch: 0x1801e43 25173571 (0: nrow: 2, level: 2)”,判断的依据是:
1、 该行缩进最靠左且靠上。
2、 其有“branch”字样,说明它不是叶子块。
3、 括号中第一个0,表示的是该索引块在本层的编号。分支块和叶子块,都是从“-1”开始往后编号,而只有根块是从0开始编号。
有两个分支块“branch: 0x1801e56 25173590 (-1: nrow: 16, level: 1)”和“branch: 0x1801e5c 25173596 (0: nrow: 6, level: 1)”。而且,从括号中最左侧的数字,我们可以知道,前者是位于左侧的分支块,后者是位于右侧的分支块。
其中位于左侧的分支块之下,共有16个叶子块;位于右侧的分支块下,有6个叶子块。
结合10200跟踪得到的数据块访问的结果 (图36),我们可以清楚地看到,最先访问的“0x01801e43”数据块,是唯一索引ind_tab_unique_c1的根块;其次访问的“0x01801e56”,是该索引中最左侧的分支块;最后访问的“0x01801e4b”是该分支块下的编号为6的叶子块。将上述访问次序,画成示意图(注,此图仅用于示意,与实际分支块和叶子块的数量并不相同,以下同),如下图所示:
图 37
进一步,我们再来看一下这三个索引块中的具体内容,理解一下上述访问次序的内在逻辑。先来看根块中的内容(请参考前面介绍的DUMP数据块到跟踪文件的方法):
图 38
我们前面在介绍索引根块结构时,提到过,根块指向下一层索引块的指针,存储在两个位置,一个是表示下一层最左侧索引块的kdxbrlmc中,另一个是以row#为代表的索引条目信息部分。在上面的这个根块中,我们可以看到索引条目中,只有一个row#0。所以,表明该根块的下一层,只有两个索引块。
而row#0中col 0中的值,是以497个字符’0’打头,以字符‘225’结尾的值。这表明,要访问大于等于这个字符串的值,需要进一步到“row#0[7550] dba: 25173596=0x1801e5c”中去查找。由于我们前边的测试SQL,要找的是以498个字符’0’打头,以字符’99’结尾的值,这个值是小于col 0中的值的,所以,要到“kdxbrlmc 25173590=0x1801e56”所指向的索引块中去查找。正因为如此,在进行索引唯一扫描时,在访问了根块后,接下来,就去访问了位于最左侧的分支块“0x1801e56”。
再来看一下最左侧分支块中的内容(为节省篇幅,这里只展示部分相关内容):
图 39
如上图所示,当访问到该分支块时,由于我们要找的字符串值的最后两位是’99’,前边的498个字符均为’0’,其大于row#0中记录的值,所以,该值一定不会在“kdxbrlmc 25173572=0x1801e44”所指向的,下一层最左侧的索引块中。前边,我们介绍过,索引分支块和根块中记录的索引列的值,是其下层索引块中的最小值。所以,row#0所指向的索引块中,记录的值一定大于等于最后两位是’15’,前边是498个字符’0’的值。那么我们要找的值,是否在这个指针所指向的索引块中呢,这还要看row#0的下一个索引条目row#1中所记录的最小值,是否大于该值。如果大于了该值,就说明它就在row#0中所指向的索引块中,反之,则需要继续向后查找。直至找到后一个索引条目已经比要寻找的值大(或者后一个索引条目已不存在)时,才去访问对应索引条目所指向的索引块。
在本例中,我们一直找到row#6时,才满足了上述条件。如下图所示:
图 40
因此,我们要去row#6中所指向的“row#6[4514] dba: 25173579=0x1801e4b”索引块中继续查找。
而且,我们可以看到,即使我们要查找的值,已经在分支块中就出现了,但仍然要访问到叶子块才算结束,而不会提前结束。