前两天研究了一下行迁移和行链接的物理结构。今天突然想到一个问题。
本文是根据行迁移和行链接一文引申而出,有些地方描述比较简单,详细的叙述可以参阅:
行迁移和行链接的物理存储:http://blog.itpub.net/post/468/19085
Oracle的行迁移本身是为了尽量避免行链接的发生,当发生行迁移时,Oracle会把数据挪到其他的block中,并在当前的block存放ROWID信息指向新的位置。这样Oracle可以保证记录本身的ROWID不变,因此对索引和其他应用程序来说,行迁移是透明的。当通过ROWID访问到原始地址时,Oracle根据原始地址保存的ROWID信息,找到新的位置从而访问到数据。
因此,我有一个疑问,如果记录本身的长度大于6个字节,也就是ROWID存储所需的空间,则这种方法不会有任何问题。但如果记录本身的长度小于6,当block空间存储满之后,如果发生了行迁移,Oracle如何存放这6个字节的ROWID信息呢。
于是,我猜想,Oracle不会将block完全装满,即使给出的PCTFREE参数等于0,Oracle也必须为行迁移时存放ROWID信息预留空间。
SQL> show parameter db_block_size
NAME TYPE VALUE
------------------------------------ ----------- -------------
db_block_size integer 8192
SQL> create table test (id varchar2(1)) pctfree 0 tablespace test;
表已创建。
SQL> insert into test select '1' from dba_objects;
已创建6270行。
SQL> commit;
提交完成。
SQL> select substr(rowid, 1, 15), count(*) from test group by substr(rowid, 1, 15);
SUBSTR(ROWID,1,15) COUNT(*)
------------------------------ ----------
AAACHTAAHAAAAAK 734
AAACHTAAHAAAAAL 734
AAACHTAAHAAAAAM 734
AAACHTAAHAAAAAN 734
AAACHTAAHAAAAAO 734
AAACHTAAHAAAAAP 734
AAACHTAAHAAAAAQ 734
AAACHTAAHAAAAAR 734
AAACHTAAHAAAAAS 398
已选择9行。
SQL> analyze table test compute statistics;
表已分析。
SQL> select avg_row_len from user_tables where table_name = 'TEST';
AVG_ROW_LEN
-----------
5
BLOCK的大小是8192。而每条记录的平均长度只有5(记录头3,长度1,数据1),加上记录的位置2个字节。每条记录约占用7字节。8192减去BLOCK HEAD(110左右)还剩8000左右,PCTFREE设置为0,至少应该可以存放1000条以上的记录,但是实际只存放了734条。剩余的空间就是为行迁移存储ROWID预留的空间。
SQL> select 8192 - 7 * 734 - 110 from dual;
8192-7*734-110
--------------
2944
SQL> select 2944/734 from dual;
2944/734
----------
4.01089918
可见Oracle为每条记录预留了4字节的空间。如果发生了行迁移,4字节加上长度和数据共6字节刚好够存储ROWID信息(记录头和记录的位置信息不变)。
SQL> alter table test modify (id varchar2(100));
表已更改。
SQL> update test set id = lpad('5', 100, '5') where rowid = 'AAACHTAAHAAAAAKAAB';
已更新 1 行。
SQL> commit;
提交完成。
SQL> alter system dump datafile 7 block 10;
系统已更改。
Dump文件:
Start dump data blocks tsn: 7 file#: 7 minblk 10 maxblk 10
buffer tsn: 7 rdba: 0x01c0000a (7/10)
scn: 0x0000.015ee903 seq: 0x01 flg: 0x06 tail: 0xe9030601
frmt: 0x02 chkval: 0xc7df type: 0x06=trans data
Block header dump: 0x01c0000a
.
.
.
block_row_dump:
tab 0, row 0, @0x1aa5
tl: 5 fb: --H-FL-- lb: 0x0 cc: 1
col 0: [ 1] 31
tab 0, row 1, @0x1141
tl: 9 fb: --H----- lb: 0x2 cc: 0
nrid: 0x01c00012.18e
tab 0, row 2, @0x1aaf
tl: 5 fb: --H-FL-- lb: 0x0 cc: 1
col 0: [ 1] 31
.
.
.
end_of_block_dump
End dump data blocks tsn: 7 file#: 7 minblk 10 maxblk 10
根据Dump文件,更新一条记录后就发生了行迁移,虽然此时Block中有足够的空间存放这100字节的数据。
SQL> update test set id = lpad('5', 100, '5');
已更新6270行。
SQL> commit;
提交完成。
SQL> select substr(rowid, 1, 15), count(*) from test group by substr(rowid, 1, 15);
SUBSTR(ROWID,1,15) COUNT(*)
------------------------------ ----------
AAACHTAAHAAAAAK 734
AAACHTAAHAAAAAL 734
AAACHTAAHAAAAAM 734
AAACHTAAHAAAAAN 734
AAACHTAAHAAAAAO 734
AAACHTAAHAAAAAP 734
AAACHTAAHAAAAAQ 734
AAACHTAAHAAAAAR 734
AAACHTAAHAAAAAS 398
已选择9行。
SQL> alter system dump datafile 7 block 10;
系统已更改。
SQL> analyze table test compute statistics;
表已分析。
SQL> analyze table test compute statistics;
表已分析。
SQL> select chain_cnt from user_tables where table_name = 'TEST';
CHAIN_CNT
----------
6270
Dump文件:
Start dump data blocks tsn: 7 file#: 7 minblk 10 maxblk 10
buffer tsn: 7 rdba: 0x01c0000a (7/10)
scn: 0x0000.015eea9c seq: 0x01 flg: 0x02 tail: 0xea9c0601
frmt: 0x02 chkval: 0x0000 type: 0x06=trans data
Block header dump: 0x01c0000a
.
.
.
block_row_dump:
tab 0, row 0, @0x1f97
tl: 9 fb: --H----- lb: 0x1 cc: 0
nrid: 0x01c00012.18f
tab 0, row 1, @0x1f8e
tl: 9 fb: --H----- lb: 0x1 cc: 0
nrid: 0x01c00012.18e
tab 0, row 2, @0x1f85
tl: 9 fb: --H----- lb: 0x1 cc: 0
nrid: 0x01c00012.190
tab 0, row 3, @0x1f7c
tl: 9 fb: --H----- lb: 0x1 cc: 0
nrid: 0x01c00012.191
tab 0, row 4, @0x1f73
tl: 9 fb: --H----- lb: 0x1 cc: 0
nrid: 0x01c00012.192
.
.
.
end_of_block_dump
End dump data blocks tsn: 7 file#: 7 minblk 10 maxblk 10
根据Dump和上面的分析后查询的结果,此时,表中所有记录都发生了行迁移。而正如预测中的一样,数据的ROWID并没有发生任何变化。
现在基本可以得出简单的结论,当记录的长度很小时(小于5),Oracle会自动为行迁移预留空间。