Oracle数据库中实例恢复起点与终点及RBA
崔华,网名 dbsnake
Oracle ACE Director,ACOUG 核心专家
编辑手记:感谢崔华授权我们独家转载其精品文章,也欢迎大家向“Oracle”社区投稿。
在Oracle数据库的恢复中,有几个概念非常重要:On Disk RBA,Low Cache RBA。从哪里开始又到哪里结束?
在这篇文章里,我们证明了On Disk RBA不是Instance Recovery的终点,Instance Recovery的终点就是Current redo log file的最尾端。
DSI403e明确指出了Low CacheRBA和On Disk RBA各自的含义:
Low Cache RBA: The low RBA of the checkpoint queues indicate where recovery can begin. All buffer swith lower RBAs were already written. Note that the buffer, or buffers, at this low RBA may be in the middle of a disk write when this value is constructed.
On Disk RBA: The on disk RBA is the highest RBA that is definitely on disk. Instance recovery must apply redo at least up to here. There are unusual circumstances where the on disk RBA may actually be lower than the low cache RBA. In these circumstances, the dirty buffers could not be written because a log force is needed. If the instance dies without a log force, then the redo at the low cache RBA may not even exist. Process death during redo generation, and buffer invalidation due to offline immediate, may remove the buffer at the on disk RBA from the checkpoint queue.
从中我们可以看出:
1、可能会出现Low Cache RBA的值已被写入control文件,但它所对应的redo record和dirty buffer还没有被写入redo log和数据文件;
2、On Disk RBA只是表示Instance Recovery的时候至少要恢复到On Disk RBA这个点;说白了,它只是真正的current redo log file的最尾端一个前镜像。
3、可能会出现On Disk RBA比Low Cache RBA小的情况,如果Oracle发现存在这种情况,则会强制写redo;
实际上,Instance Recovery的起点是Low Cache RBA和Thread Checkpoint RBA中的最大值,Instance Recovery的终点就是current redo log file的最尾端。
Oracle在做Instance Recovery的时候是受隐含参数_two_pass的控制,其默认为true,这表示要Oracle做Instance Recovery的时候是采用Two Pass Recovery,即要扫描current redo log file两次。
Two Pass Recovery的核心是在做Instance Recovery时要去掉那些已经被写入数据文件的数据块所对应的redo record,Oracle称这些redo record为Block Written Record (BWR)。BWR所对应的op codes可能是23.1(这个我不确定,是猜的),如下是我从redo中dump的结果:
证明过程:我们同时开三个session,先在session 1做一些准备工作:
Session 1:
创建一个自己到自己的db link,这是为了规避Oracle对PL/SQL循环中commit的优化过程,确保后续的PL/SQL循环里每一次commit后redo都能被及时写入redo log:
SQL> create public database link CUIHUA112 connect to SCOTT identified by tiger using ‘cuihua112’;
Database link created
验证一下上述自己到自己的db link是否有效:
SQL> select count() from dba_objects;
COUNT()
———-
71962
SQL> select count() from dba_objects@cuihua112;
COUNT()
———-
71962
创建一个测试表T:
SQL> create table t(id number);
Table created
创建一个用于测试的存储过程,在PL/SQL循环里每隔1秒钟就向表T插入一条数据,每插入一条就commit一次:
SQL> create or replace procedure p_instance_recovery_demo is
2 i number;
3 begin
4 for i in 1…100 loop
5 insert into t@cuihua112 values (i);
6 commit;
7 dbms_lock.sleep(1);
8 end loop;
9 end p_instance_recovery_demo;
10 /
Procedure created
准备工作做完了,我开始在session 1中执行上述存储过程:
Session 1:
SQL> exec p_instance_recovery_demo;
……这里正在执行
然后我们到session 2中去查询表T的插入数据的情况,并同时确定T1中插入数据的物理存储位置:
Session 2:
SQL> select t.id,dbms_rowid.rowid_relative_fno(rowid)||’_’||dbms_rowid.rowid_block_number(rowid) location from t;
ID LOCATION
———- ——————————————————————————–
1 4_87284
2 4_87284
3 4_87284
4 4_87284
……省略显示部分内容
27 4_87284
28 4_87284
29 4_87284
30 4_87284
30 rows selected
从上述结果中我们可以看到,现在表T1中已被插入了30条记录,这些记录的物理存储位置都是file 4,block 87274。
在session 3中我执行一次thread checkpoint,同时马上紧跟着执行shutdown abort:
Session 3:
SQL> alter system checkpoint;
系统已更改。
SQL> shutdown abort;
ORACLE 例程已经关闭。
回到session 1,我们发现上述存储过程p_instance_recovery_demo已被Oracle中断:
Session 1:
SQL> exec p_instance_recovery_demo;
begin p_instance_recovery_demo; end;
ORA-03113: 通信通道的文件结尾
进程 ID: 5816
会话 ID: 24 序列号: 3
我们现在新开一个command窗口,先用BBED去看一下现在数据文件上的Thread Checkpoint RBA是多少。这里有一点背景知识需要交待,在Oracle 10g以上的版本里,Checkpoint RBA的位置跟Oracle 9i是不一样的,其起始位置是在数据文件头的offset 500处:
BBED> p kcvfhckp
回到上述command窗口,这里因为是将windows平台上Oracle 9i的BBED用于Oracle 11gR2,所以定位数据块的时候block number要往后错一位:
BBED> set file 4 block 2
FILE# 4
BLOCK# 2
BBED> set offset 500
OFFSET 500
BBED> dump
可以看到现在的Thread Checkpoint RBA是000001c3.0000ff0f.0010。
我们在来看一下表T的数据插入情况:
BBED> set file 4 block 87285
FILE# 4
BLOCK# 87285
BBED> map /v
File: C:\APP\CUIHUA\ORADATA\CUIHUA112\USERS01.DBF (4)
Block: 87285 Dba:0x010154f5
————————————————————
KTB Data Block (Table/Cluster)
struct kcbh, 20 bytes @0
ub1 type_kcbh @0
……省略显示部分内容
ub2 spare3_kcbh @18
struct kdbt[1], 4 bytes @114
b2 kdbtoffs @114
b2 kdbtnrow @116
sb2 kdbr[49] @118
ub1 freespace[7678] @216
ub1 rowdata[294] @7894
ub4 tailchk @8188
很明显,现在表T在磁盘上只有49条记录。
我们来看一下这些记录:
BBED> p *kdbr[0]
rowdata[288]
————
ub1 rowdata[288] @8182 0x2c
BBED> x /rn
rowdata[288] @8182
————
flag@8182: 0x2c (KDRHFL, KDRHFF, KDRHFH)
lock@8183: 0x00
cols@8184: 1
col 0[2] @8185: 1
BBED> p *kdbr[1]
rowdata[282]
————
ub1 rowdata[282] @8176 0x2c
BBED> x /rn
rowdata[282] @8176
————
flag@8176: 0x2c (KDRHFL, KDRHFF, KDRHFH)
lock@8177: 0x00
cols@8178: 1
col 0[2] @8179: 2
BBED> p kdbr[48]
rowdata[0]
———-
ub1 rowdata[0] @7894 0x2c
BBED> x /rn
rowdata[0] @7894
———-
flag@7894: 0x2c (KDRHFL, KDRHFF, KDRHFH)
lock@7895: 0x01
cols@7896: 1
col 0[2] @7897: 49
从上述内容我们可以看出,表T现在在磁盘上的第1条记录的id列的值为1,第2条记录的id列的值为2,第49条记录的id列的值为49,这和我们预期的一致。
现在我们将上述shutdown abort后的库启动到mount状态,然后对control文件和current redo log file执行dump操作:
E:>sqlplus /nolog
SQLPlus: Release 11.2.0.1.0 Production on 星期三 8月 1 10:20:58 2012
Copyright © 1982, 2010, Oracle. All rights reserved.
SQL> conn / as sysdba;
已连接到空闲例程。
SQL> startup mount
ORACLE 例程已经启动。
Total System Global Area 640286720 bytes
Fixed Size 1376492 bytes
Variable Size 234884884 bytes
Database Buffers 398458880 bytes
Redo Buffers 5566464 bytes
数据库装载完毕。
SQL> select group# from vlog where status=’CURRENT’;
GROUP#
———-
1
SQL> select member from vlogfile where group#=1;
MEMBER
——————————————————————————–
C:\APP\CUIHUA\ORADATA\CUIHUA112\REDO01.LOG
SQL> oradebug setmypid
已处理的语句
SQL> alter session set events ‘immediate trace name controlf level 3’;
会话已更改。
SQL> alter system dump logfile ‘C:\APP\CUIHUA\ORADATA\CUIHUA112\REDO01.LOG’;
系统已更改。
SQL> oradebug tracefile_name
c:\app\cuihua\diag\rdbms\cuihua112\cuihua112\trace\cuihua112_ora_4964.trc
我们从上述trace文件c:\app\cuihua\diag\rdbms\cuihua112\cuihua112\trace\cuihua112_ora_4964.trc中就可以看到现在上述库的控制文件和current redo log的内容。
先来看上述trace文件中包含的对控制文件的dump内容里的Low Cache RBA和On Disk RBA:
现在的Low Cache RBA是0x1c3.ff1a.0,刚才我们已经用BBED看到Thread Checkpoint RBA是000001c3.0000ff0f.0010,很显然现在Low Cache RBA大于Thread Checkpoint RBA,所以Oracle在做Instance Recovery的时候就会以Low Cache RBA为准。
SQL> select to_number(‘1c3′,’XXXXXXXX’) from dual;
TO_NUMBER(‘1C3′,’XXXXXXXX’)
—————————
451
SQL> select to_number(‘ff1a’,’XXXXXXXX’) from dual;
TO_NUMBER(‘FF1A’,’XXXXXXXX’)
—————————-
65306
从上面的结果里我们可以看出,Low Cache RBA是0x1c3.ff1a.0,转换过来就是Low Cache RBA的logfile sequence是451,logfile block number是65306。
另外,我们可以看到现在控制文件里记录的On Disk RBA是0x1c3.ff1d.0,转换过来就是On Disk RBA的logfile sequence是451,logfile block number是65309:
SQL> select to_number(‘1c3′,’XXXXXXXX’) from dual;
TO_NUMBER(‘1C3′,’XXXXXXXX’)
—————————
451
SQL> select to_number(‘ff1d’,’XXXXXXXX’) from dual;
TO_NUMBER(‘FF1D’,’XXXXXXXX’)
—————————-
65309
我们再来看上述trace文件中包含的对currentredo log file的dump内容里的尾端redorecord的情况。 首先来看currentredo log file的尾端的插入记录:
REDORECORD – Thread:1 RBA: 0x0001c3.0000ff31.0110 LEN: 0x0140 VLD:0x01
SCN:0x0001.c018412d SUBSCN: 2 08/01/2012 09:56:46
CHANGE #1TYP:0 CLS:25 AFN:3 DBA:0x00c000c0 OBJ:4294967295 SCN:0x0001.c018412d SEQ:1OP:5.2 ENC:0 RBL:0
ktudhredo: slt: 0x0013 sqn: 0x00000000 flg: 0x0002 siz: 80 fbi: 0
uba:0x00c019b8.02d7.16 pxid: 0x0000.000.00000000
CHANGE #2TYP:0 CLS:26 AFN:3 DBA:0x00c019b8 OBJ:4294967295 SCN:0x0001.c018412d SEQ:1OP:5.1 ENC:0 RBL:0
ktudbredo: siz: 80 spc: 5148 flg: 0x1022 seq: 0x02d7 rec: 0x16
xid: 0x0005.013.000009f4
ktuburedo: slt: 19 rci: 0 opc: 11.1 objn: 82084 objd: 82084 tsn: 4
Undotype: Regular undo Undotype: Last buffer split: No
TablespaceUndo: No
0x00000000
KDO undorecord:
KTB Redo
op:0x04 ver: 0x01
compatbit: 4 (post-11) padding: 0
op:L itl: xid: 0x0003.007.0000091e uba: 0x00c000ec.0451.0f
flg:C— lkc: 0 scn:0x0001.c0184129
KDO Opcode: DRP row dependencies Disabled
xtype:XA flags: 0x00000000 bdba: 0x010154f4 hdba: 0x010154f2
itli:2 ispac: 0 maxfr: 4858
tabn: 0slot: 51(0x33)
CHANGE #3TYP:2 CLS:1 AFN:4 DBA:0x010154f4 OBJ:82084 SCN:0x0001.c018412cSEQ:1OP:11.2 ENC:0 RBL:0
KTB Redo
op:0x01 ver: 0x01
compatbit: 4 (post-11) padding: 0
op:F xid: 0x0005.013.000009f4 uba:0x00c019b8.02d7.16
KDO Opcode: IRP row dependencies Disabled
xtype:XA flags: 0x00000000 bdba: 0x010154f4 hdba: 0x010154f2
itli:2 ispac: 0 maxfr: 4858
tabn: 0slot: 51(0x33) size/delt: 6
fb: –H-FL–lb: 0x2 cc: 1
null: –
col 0:[ 2] c1 35
从上述内容我们可以看到,最后一条对object82084的插入记录(这里op codes为11.2,表示是insert操作)的第1列(即表T的id列)的插入值是0xc135,它所对应的RBA是0x0001c3.0000ff31.0110:
对象82084就是表T:
SQL> select owner,object_name fromdba_objects where object_id=82084;
OWNER OBJECT_NAME
————————————————–
SCOTT T
0xc135就是52:
SQL> select utl_raw.cast_to_number(‘c135’)from dual;
UTL_RAW.CAST_TO_NUMBER(‘C135’)
——————————
52
注意到这里差异就产生了。我们刚才用BBED查看了表T在磁盘上的最后一条记录,其id值是49。但这里对currentredo log file的dump清晰的告诉我们,上述表T的最后一条被成功插入的记录的id值是52。也就是说,id为50、51和52的那三条记录还在buffercache里,还没有被写回到数据文件。
另外我们刚才已经从对控制文件的dump内容看到On Disk RBA的值是0x1c3.ff1d.0,而上述插入id值为52的这条redo record的RBA是0x0001c3.0000ff31.0110,即现在的On DiskRBA小于id值为52的这条redorecord所在的RBA。如果Oracle在做InstanceRecovery的时候只恢复到On DiskRBA,那么就意味着id为52的这条记录就真的丢掉了。
我们接着来看currentredo log file尾端的最后一条redorecord:
从上面的内容我们可以看到,现在currentredo log file尾端的最后一条redorecord对应的RBA是0x0001c3.0000ff34.0010,翻译过来就是currentredo log file尾端的最后一条redorecord对应的logfilesequence是451,logfileblock number是65332:
SQL> select to_number(‘1c3′,’XXXXXXXX’)from dual;
TO_NUMBER(‘1C3′,’XXXXXXXX’)
—————————
451
SQL> select to_number(‘ff34′,’XXXXXXXX’)from dual;
TO_NUMBER(‘FF34′,’XXXXXXXX’)
—————————-
65332
好了,我们现在回到上述command窗口来把上述数据库open。在open完毕后我们马上紧跟着执行对当前控制文件的dump操作:
SQL> alter database open;
数据库已更改。
SQL> alter session set events ‘immediatetrace name controlf level 3’;
会话已更改。
很显然,Oracle在open上述库的过程中做了Instance Recovery,我们现在去看相关的alert log里记录的内容:
从上述alert log我们可以知道,Oracle做InstanceRecovery的起点是logseq 451,block 65306;终点是logseq451, block 65333。
从之前的对控制文件的dump我们可以看到控制文件里记录的Low CacheRBA是0x1c3.ff1a.0,转换过来就是Low CacheRBA的logfile sequence是451,logfile block number是65306。”这和alert log里记录的Instance Recovery的起点一致,即InstanceRecovery的起点就是Low CacheRBA和Thread Checkpoint RBA中的最大值。
另外,控制文件里记录的On DiskRBA是0x1c3.ff1d.0,转换过来就是On DiskRBA的logfile sequence是451,logfile block number是65309。显然这个和alert log里记录的InstanceRecovery的终点不一致,所以On DiskRBA不是InstanceRecovery的终点。
从之前的对currentredo log的dump我们可以看到在shutdownabort时current redo log的最后一条redorecord所对应的RBA是0x0001c3.0000ff34.0010,转换过来就是currentredo log file的最后一条redorecord对应的logfile sequence是451,logfile block number是65332,这个再往后错一位就匹配上了alert log里记录的Instance Recovery的终点。即InstanceRecovery的终点就是currentredo log file的最尾端。
我们来看一下开库后现在表T的数据情况:
SQL> select count() from scott.t;
COUNT()
———-
52
SQL> select max(id) from scott.t;
MAX(ID)
———-
52
显然,最后插入的那条id为52的记录没有丢失。
至此,我们已经完成了整个的证明过程。最后我们来看一下我们开库后马上做的那个对控制文件的dump的内容:
从上述内容我们可以看到,Oracle在Open上述库后马上递增了Low CacheRBA和On Disk RBA。Oracle做完了所有的Instance Recovery工作才用当时的recovery checkpoint去更新控制文件。
崔华:Oracle 里的哈希连接原理解析
Oracle ACE Director,ACOUG 核心专家
编辑手记:感谢崔华授权我们独家转载其精品文章,也欢迎大家向“Oracle”社区投稿。
哈希连接(HASH JOIN)是一种两个表在做表连接时主要依靠哈希运算来得到连接结果集的表连接方法。
在 Oracle 7.3之前,Oracle 数据库中的常用表连接方法就只有排序合并连接和嵌套循环连接这两种,但这两种表连接方法都有其明显缺陷:
1.对于排序合并连接,如果两个表在施加了目标 SQL 中指定的谓词条件(如果有的话)后得到的结果集很大且需要排序的话,则这种情况下的排序合并连接的执行效率一定是很差的;
2.而对于嵌套循环连接,如果驱动表所对应的驱动结果集的记录数很大,即便在被驱动表的连接列上存在索引,此时使用嵌套循环连接的执行效率也同样会很差。
为了解决排序合并连接和嵌套循环连接在上述情形下执行效率不高的问题,同时也为了给优化器提供一种新的选择,Oracle 在 Oracle 7.3 中引入了哈希连接。从理论上来说,哈希连接的执行效率会比排序合并连接和嵌套循环连接的执行效率要高,当然,实际情况并不总是这样。
在 Oracle 10g 及其以后的 Oracle 数据库版本中,优化器(实际上是 CBO,因为哈希连接仅适用于 CBO)在解析目标 SQL 时是否考虑哈希连接是受限于隐含参数 _HASH_JOIN_ENABLED,而在 Oracle 10g 以前的 Oracle 数据库版本中,CBO 在解析目标 SQL 时是否考虑哈希连接是受限于参数 HASH_JOIN_ENABLED。
_HASH_JOIN_ENABLED 的默认值是 TRUE,表示允许 CBO 在解析目标 SQL时考虑哈希连接。当然,即使你将该参数的值改成了 FALSE,我们使用 USE_HASH Hint 依然可以让 CBO 在解析目标 SQL 时考虑哈希连接,这说明 USE_HASH Hint 的优先级高于参数 _HASH_JOIN_ENABLED。
如果两个表(这里将它们分别命名为表 T1 和表 T2)在做表连接时使用的是哈希连接,则 Oracle 在做哈希连接时会依次顺序执行如下步骤:
1.首先 Oracle 会根据参数 HASH_AREA_SIZE、DB_BLOCK_SIZE 和_HASH_MULTIBLOCK_IO_COUNT 的值来决定 Hash Partition 的数量(Hash Partition 是一个逻辑上的概念,所有 Hash Partition 的集合就被称之为 Hash Table,即一个 Hash Table 是由多个 Hash Partition 所组成,而一个 Hash Partition 又是由多个 Hash Bucket 所组成);
2.表T1和T2在施加了目标 SQL 中指定的谓词条件(如果有的话)后,得到的结果集中数据量较小的会被 Oracle 选为哈希连接的驱动结果集,这里我们假设 T1 所对应的结果集的数据量相对较小,记为 S;T2 所对应的结果集的数据量相对较大,记为 B;显然这里 S 是驱动结果集,B 是被驱动结果集;
3.遍历 S,读取 S 中的每一条记录,并对 S 中的每一条记录按照该记录在表 T1 中的连接列做哈希运算,这个哈希运算会使用两个内置哈希函数,这两个哈希函数会同时对该连接列计算哈希值,把这两个内置哈希函数分别记为 hash_func_1 和 hash_func_2,计算的哈希值分别记为 hash_value_1 和 hash_value_2;
4.按照 hash_value_1 的值把相应的 S 中的对应记录存储在不同 Hash Partition 的不同 Hash Bucket 里,同时和该记录存储在一起的还有该记录用 hash_func_2 计算出来的 hash_value_2 的值。注意,存储在 Hash Bucket 里的记录并不是目标表的完整行记录,而是只需要存储位于目标 SQL 中的跟目标表相关的查询列和连接列就足够了;把 S 所对应的每一个 Hash Partition 记为 Si;
5.在构建 Si 的同时,Oracle 会构建一个位图(BITMAP),这个位图用来标记 Si 所包含的每一个 Hash Bucket 是否有记录(即记录数是否大于0);
6.如果 S 的数据量很大,那么在构建 S 所对应的 Hash Table 时,就可能会出现 PGA 的工作区(WORK AREA)被填满的情况,这时候 Oracle 会把工作区中现有的 Hash Partition 中包含记录数最多的 Hash Partition 写到磁盘上(TEMP 表空间);接着 Oracle 会继续构建 S 所对应的 Hash Table,在继续构建的过程中,如果工作区又满了,则 Oracle 会继续重复上述挑选包含记录数最多的 Hash Partition 并写回到磁盘上的动作;如果要构建的记录所对应的 Hash Partition 已经事先被 Oracle 写回到了磁盘上,则此时 Oracle 就会去磁盘上更新该 Hash Partition,即会把该条记录和 hash_value_2 直接加到这个已经位于磁盘上的 Hash Partition 的相应 Hash Bucket 中;注意,极端情况下可能会出现只有某个 Hash Partition 的部分记录还在内存中,该 Hash Partition 的剩余部分和余下的所有 Hash Partition 都已经被写回到磁盘上;
7.上述构建 S 所对应的 Hash Table 的过程会一直持续下去,直到遍历完 S 中的所有记录为止;
8.接着,Oracle 会对所有的 Si 按照它们所包含的记录数来排序,然后 Oracle 会把这些已经排好序的 Hash Partition 按顺序依次、并且尽可能的全部放到内存中(PGA 的工作区),当然,如果实在放不下的话,放不下的那部分 Hash Partition 还是会位于磁盘上。我认为这个按照 Si 的记录数来排序的动作不是必须要做的,因为这个排序动作的根本目的就是为了尽可能多的把那些记录数较小的 Hash Partition 保留在内存中,而将那些已经被写回到磁盘上、记录数较大且现有内存已经放不下的 Hash Partition 保留在磁盘上,显然,如果所有的 Si 本来就都在内存中,也没发生过将 Si 写回到磁盘的操作,那这里根本就不需要排序了。
9.至此 Oracle 已经处理完 S,现在可以来开始处理 B 了;
10.Oracle 会遍历 B,读取 B 中的每一条记录,并对 B 中的每一条记录按照该记录在表 T2 中的连接列做哈希运算,这个哈希运算和步骤3中的哈希运算是一模一样的,即这个哈希运算还是会用步骤3中的 hash_func_1 和 hash_func_2,并且也会计算出两个哈希值 hash_value_1 和hash_value_2;接着 Oracle 会按照该记录所对应的哈希值 hash_value_1 去 Si 里找匹配的 Hash Bucket;如果能找到匹配的 Hash Bucket,则 Oracle 还会遍历该 Hash Bucket 中的每一条记录,并会校验存储于该 Hash Bucket 中的每一条记录的连接列,看是否是真的匹配(即这里要校验 S 和 B 中的匹配记录所对应的连接列是否真的相等,因为对于 Hash 运算而言,不同的值经过哈希运算后的结果可能是一样的),如果是真的匹配,则上述 hash_value_1 所对应 B 中的记录的位于目标 SQL 中的查询列和该 Hash Bucket 中的匹配记录便会组合起来,一起作为满足目标 SQL 连接条件的记录返回;如果找不到匹配的 Hash Bucket,则 Oracle 就会去访问步骤5中构建的位图,如果位图显示该 Hash Bucket 在 Si 中对应的记录数大于0,则说明该 Hash Bucket 虽然不在内存中,但它已经被写回到了磁盘上,则此时 Oracle就会按照上述 hash_value_1 的值把相应 B 中的对应记录也以 Hash Partition 的方式写回到磁盘上,同时和该记录存储在一起的还有该记录用 hash_func_2 计算出来的 hash_value_2 的值;如果位图显示该 Hash Bucket 在 Si 中对应的记录数等于0,则 Oracle 就不用把上述 hash_value_1所对应 B 中的记录写回到磁盘上了,因为这条记录必然不满足目标 SQL 的连接条件;这个根据位图来决定是否将上述 hash_value_1 所对应B中的记录写回到磁盘的动作就是所谓的“位图过滤”;我们把B所对应的每一个 Hash Partition 记为 Bj;
11.上述去 Si 中查找匹配 Hash Bucket 和构建 Bj 的过程会一直持续下去,直到遍历完 B 中的所有记录为止;
12.至此 Oracle 已经处理完所有位于内存中的 Si 和对应的 Bj,现在只剩下位于磁盘上的 Si 和 Bj 还未处理;
13.因为在构建 Si 和 Bj 时用的是同样的哈希函数 hash_func_1 和hash_func_2,所以 Oracle 在处理位于磁盘上的 Si 和 Bj 的时候可以放心的配对处理,即只有对应 Hash Partition Number 值相同的 Si 和 Bj 才可能会产生满足连接条件的记录;这里我们用 Sn 和 Bn 来表示位于磁盘上且对应 Hash Partition Number 值相同的 Si 和 Bj;
14.对于每一对儿 Sn 和 Bn,它们之中记录数较少的会被当作驱动结果集,然后 Oracle 会用这个驱动结果集的 Hash Bucket 里记录的 hash_value_2 来构建新的 Hash Table,另外一个记录数较大的会被当作被驱动结果集,然后 Oracle会用这个被驱动结果集的 Hash Bucket 里记录的 hash_value_2 去上述构建的新 Hash Table 中找匹配记录;注意,对每一对儿 Sn 和 Bn 而言,Oracle 始终会选择它们中记录数较少的来作为驱动结果集,所以每一对儿 Sn 和 Bn 的驱动结果集都可能会发生变化,这就是所谓的“动态角色互换”;
15.步骤14中如果存在匹配记录,则该匹配记录也会作为满足目标 SQL 连接条件的记录返回;
16.上述处理 Sn 和 Bn 的过程会一直持续下去,直到遍历完所有的 Sn 和 Bn 为止。
对于哈希连接的优缺点及适用场景,我们有如下总结:
哈希连接不一定会排序,或者说大多数情况下都不需要排序;
哈希连接的驱动表所对应的连接列的可选择性应尽可能的好,因为这个可选择性会影响对应 Hash Bucket 中的记录数,而 Hash Bucket 中的记录数又会直接影响从该 Hash Bucket 中查找匹配记录的效率;如果一个 Hash Bucket 里所包含的记录数过多,则可能会严重降低所对应哈希连接的执行效率,此时典型的表现就是该哈希连接执行了很长时间都没有结束,数据库所在 database server 上的 CPU 占用率很高,但目标 SQL 所消耗的逻辑读却很低,因为此时大部分时间都耗费在了遍历上述 Hash Bucket 里的所有记录上,而遍历 Hash Bucket 里记录这个动作是发生在 PGA 的工作区里,所以不耗费逻辑读;
哈希连接只适用于 CBO、它也只能用于等值连接条件(即使是哈希反连接,Oracle 实际上也是将其转换成了等价的等值连接);
哈希连接很适合于一个小表和大表之间的表连接,特别是在小表的连接列的可选择性非常好的情况下,这时候哈希连接的执行时间就可以近似看作是和全表扫描那个大表所耗费的时间相当;
当两个表做哈希连接时,如果这两个表在施加了目标 SQL 中指定的谓词条件(如果有的话)后得到的结果集中数据量较小的那个结果集所对应的 Hash Table 能够完全被容纳在内存中时(PGA 的工作区),则此时的哈希连接的执行效率会非常高。
可以借助于10104事件所产生的 trace 文件来观察目标 SQL 在做哈希连接时的大致过程和一些统计信息(比如用了多少个 Hash Partition、多个少 Hash Bucket 以及各个 Hash Bucket 都分别有多少条记录等),10104 事件在我们实际诊断哈希连接的性能问题时非常有用。
使用 10104 事件观察目标 SQL 做哈希连接的具体过程为:
oradebug setmypid
oradebug event 10104 trace name context forever, level 1
set autotrace traceonly
实际执行目标 SQL(必须要实际执行该 SQL,不能用 explain plan for)
oradebug tracefile_name
一个典型的 10104 事件所产生的 trace 文件内容为如下所示:
kxhfInit(): enter
kxhfInit(): exit
*** RowSrcId: 1 HASH JOIN STATISTICS (INITIALIZATION) ***
Join Type: INNER join
Original hash-area size: 3642760
Memory for slot table: 2826240
Calculated overhead for partitions and row/slot managers: 816520
Hash-join fanout: 8
Number of partitions: 8
Number of slots: 23
Multiblock IO: 15
Block size(KB): 8
Cluster (slot) size(KB): 120
Minimum number of bytes per block: 8160
Bit vector memory allocation(KB): 128
Per partition bit vector length(KB): 16
……省略显示部分内容
Slot table resized: old=23 wanted=12 got=12 unload=0
*** RowSrcId: 1 HASH JOIN RESIZE BUILD (PHASE 1) ***
Total number of partitions: 8
Number of partitions which could fit in memory: 8
Number of partitions left in memory: 8
Total number of slots in in-memory partitions: 8
kxhfResize(enter): resize to 14 slots (numAlloc=8, max=12)
kxhfResize(exit): resized to 14 slots (numAlloc=8, max=14)
set work area size to: 2215K (14 slots)
*** RowSrcId: 1 HASH JOIN BUILD HASH TABLE (PHASE 1) ***
Total number of partitions: 8
Number of partitions left in memory: 8
Total number of rows in in-memory partitions: 1000
(used as preliminary number of buckets in hash table)
Estimated max # of build rows that can fit in avail memory: 79800
Partition Distribution
Partition:0 rows:120 clusters:1 slots:1 kept=1
Partition:1 rows:122 clusters:1 slots:1 kept=1
……省略显示部分内容
Partition:6 rows:118 clusters:1 slots:1 kept=1
Partition:7 rows:137 clusters:1 slots:1 kept=1
*** (continued) HASH JOIN BUILD HASH TABLE (PHASE 1) ***
Revised number of hash buckets (after flushing): 1000
Allocating new hash table.
*** (continued) HASH JOIN BUILD HASH TABLE (PHASE 1) ***
Requested size of hash table: 256
Actual size of hash table: 256
Number of buckets: 2048
Match bit vector allocated: FALSE
*** (continued) HASH JOIN BUILD HASH TABLE (PHASE 1) ***
Total number of rows (may have changed): 1000
Number of in-memory partitions (may have changed): 8
Final number of hash buckets: 2048
Size (in bytes) of hash table: 8192
qerhjBuildHashTable(): done hash-table on partition=7, index=0 last_slot#=3 rows=137 total_rows=137
qerhjBuildHashTable(): done hash-table on partition=6, index=1 last_slot#=4 rows=118 total_rows=255
……省略显示部分内容
qerhjBuildHashTable(): done hash-table on partition=1, index=6 last_slot#=2 rows=122 total_rows=880
qerhjBuildHashTable(): done hash-table on partition=0, index=7 last_slot#=5 rows=120 total_rows=1000
kxhfIterate(end_iterate): numAlloc=8, maxSlots=14
*** (continued) HASH JOIN BUILD HASH TABLE (PHASE 1) ***
Hash table
NOTE: The calculated number of rows in non-empty buckets may be smaller
than the true number.
Number of buckets with 0 rows: 1249
Number of buckets with 1 rows: 626
Number of buckets with 2 rows: 149
Number of buckets with 3 rows: 21
Number of buckets with 4 rows: 3
Number of buckets with 5 rows: 0
……省略显示部分内容
Number of buckets with between 90 and 99 rows: 0
Number of buckets with 100 or more rows: 0
Hash table overall statistics
Total buckets: 2048 Empty buckets: 1249 Non-empty buckets: 799
Total number of rows: 1000
Maximum number of rows in a bucket: 4
Average number of rows in non-empty buckets: 1.251564
Disabled bitmap filtering: filtered rows=0 minimum required=50 out of=1000
qerhjFetch: max probe row length (mpl=0)
*** RowSrcId: 1, qerhjFreeSpace(): free hash-join memory
kxhfRemoveChunk: remove chunk 0 from slot table
注意到上述显示内容中我粗体标出的部分,如
“Number of in-memory partitions (may have changed):8”、
“Final number of hash buckets: 2048”、
“Total buckets: 2048 Empty buckets: 1249 Non-empty buckets: 799”、
“Total number of rows: 1000”、
“Maximum number of rows in a bucket:4”、
“Disabled bitmap filtering: filtered rows=0 minimum required=50 out of=1000”
等,这说明上述哈希连接驱动结果集的记录数为1000,共有8个 Hash Partition、2048个 Hash Bucket,这2048个 Hash Bucket 中有1249个是空的(即没有记录)、799个有记录,包含记录数最多的一个 Hash Bucket 所含记录的数量为4以及上述哈希连接并没有启用位图过滤。




