一、行迁移 / 行链接的介绍
在实际的工作中我们经常会碰到一些 Oracle 数据库性能较低的问题,当然,引起 Oracle 数据库性能较低的原因是多方面的,我们能够通过一些正确的设计和诊断来尽量的避免一些 Oracle 数据库性能不好, Row Migration ( 行迁移 ) & Row Chaining ( 行链接 ) 就是其中我们可以尽量避免的引起 Oracle 数据库性能低下的潜在问题。通过合理的诊断行迁移 / 行链接,我们可以较大幅度上提高 Oracle 数据库的性能。
那究竟什么是行迁移 / 行链接呢,先让我们从 Oracle 的 block 开始谈起。
操作系统的最小读写操作单元是操作系统的 block ,所以当创建一个 Oracle 数据库的时候我们应该讲数据库的 block size 设置成为操作系统的 block size 的整数倍, Oracle block 是 Oracle 数据库中读写操作的最小单元, Oracle9i 之前的 Oracle 数据库版本中 Oracle block 一旦在创建数据库的时候被设定后就没法再更改。为了在创建数据库之前确定一个合理的 Oracle block 的大小,我们需要考虑一些因素,例如数据库本身的大小以及并发事务的数量等。使用一个合适的 Oracle block 大小对于数据库的调优是非常重要的。 Oracle block 的结构如下图所示:
图一: Oracle Block 结构图
由上图我们可以看出,一个 Oracle block 由三个部分组成,分别是数据块头、自由空间、实际数据三部份组成。
数据块头:主要包含有数据块地址的一些基本信息和段的类型,以及表和包含有数据的实际行的地址。
自由空间:是指可以为以后的更新和插入操作分配的空间,大小由 PCTFREE 和 PCTUSED 两个参数影响。
实际数据:是指在行内存储的实际数据。
当创建或者更改任何表和索引的时候, Oracle 在空间控制方面使用两个存储参数:
PCTFREE: 为将来更新已经存在的数据预留空间的百分比。
PCTUSED: 用于为插入一新行数据的最小空间的百分比。这个值决定了块的可用状态。可用的块时可以执行插入的块,不可用状态的块只能执行删除和修改,可用状态的块被放在 freelist 中。
当表中一行的数据不能在一个数据 block 中放入的时候,这个时候就会发生两种情况,一种是行链接,另外一种就是行迁移了。
行链接产生在第一次插入数据的时候如果一个 block 不能存放一行记录的情况下。这种情况下, Oracle 将使用链接一个或者多个在这个段中保留的 block 存储这一行记录,行链接比较容易发生在比较大的行上,例如行上有 LONG 、 LONG RAW 、 LOB 等数据类型的字段,这种时候行链接是不可避免的会产生的。
当一行记录初始插入的时候事可以存储在一个 block 中的,由于更新操作导致行长增加了,而 block 的自由空间已经完全满了,这个时候就产生了行迁移。在这种情况下, Oracle 将会迁移整行数据到一个新的 block 中(假设一个 block 中可以存储下整行数据), Oracle 会保留被迁移行的原始指针指向新的存放行数据的 block ,这就意味着被迁移行的 ROW ID 是不会改变的。
当发生了行迁移或者行链接,对这行数据操作的性能就会降低,因为 Oracle 必须要扫描更多的 block 来获得这行的信息。
下面举例来具体说明行迁移 / 行链接的产生过程。
先创建一个 pctfree 为 20 和 pctused 为 50 的测试表:
create table test (
col1 char(20),
col2 number)
storage (
pctfree 20
pctused 50);
当插入一条记录的时候, Oracle 会在 free list 中先去寻找一个自由的块,并且将数据插入到这个自由块中。而在 free list 中存在的自由的块是由 pctfree 值决定的。初始的空块都是在 free list 中的,直到块中的自由空间达到 pctfree 的值,此块就会从 free list 中移走,而当此块中的使用空间低于 pctused 的时候,此块又被重新放到 free list 中。
Oracle 使用 free list 机制可以大大的提高性能,对于每次的插入操作, Oracle 只需要查找 free list 就可以了,而不是去查找所有的 block 来寻找自由空间。
假设第一次插入数据使用的一个空的 block ,如下图所示:
图二: Oracle 空的 block 结构图
假设插入第一条记录的时候占用一个 block 的 10% 的空间(除去 block 头占去的大小),剩余的自由空间 90% 大于 pctfree20% ,因此这个 block 还将继续为下次的插入操作提供空间。
图三:插入 10% 后的 Oracle block 结构图
再连续插入七条记录,使 block 的剩余自由空间剩下 20% ,此时,这个 block 将要从 free list 中移走,如果再插入记录, Oracle 将再 free list 中寻找下一个空余的 block 去存放后来插入的数据。
图四:插入 80% 后的 Oracle block 结构图
此时如果去更新第一条插入的记录,使其行长增加 15%,Oracle 将会使用这个 block 中剩余的 20% 的自由空间来存放此行数据,如果再更新第二条记录,同样的使其行长增加 15%, 而此 block 中只剩下 5% 的自由空间,不够存放更新的第二条记录,于是 Oracle 会在 free list 中寻找一个有自由空间( 10%+15% )的 block 来存放这行记录的 block 去存储,在原来的 block 中保存了指向新的 block 的指针,原来这行记录的 ROW ID 保持不变,这个时候就产生了行迁移。
而当我们插入一条新纪录的时候,如果一个 blcok 不足以存放下这条记录, Oracle 就会寻找一定数量的 block 一起来容纳这条新的记录,这个时候就产生了行链接,行链接主要产生在 LOB 、 CLOB 、 BLOB 和大的 VA 行链接 HAR2 数据类型上。
具体我们通过下面的一个试验来查看行链接和行迁移是如何产生并在数据文件中体现出来的。
先查看 ALLAN 这个表空间的数据文件号,为了便于测试,我只建立了一个数据文件。
SQL> select file_id from dba_data_files where tablespace_name='ALLAN';
FILE_ID
----------
23
创建一个测试表 test :
SQL> create table test ( x int primary key, a char(2000), b char(2000), c char(2000), d char(2000), e char(2000) ) tablespace allan;
Table created.
因为我的数据库的 db_block_size 是 8K ,所以我创建的表有五个字段,每个占 2000 个字节,这样一行记录大约 10K, 就能超过一个 block 的大小了。
然后插入一行记录,只有一个字段的:
SQL> insert into test(x) values (1);
1 row created.
SQL> commit;
Commit complete.
查找这行记录所在的 block ,并 dump 出来:
SQL> select dbms_rowid.rowid_block_number(rowid) from test;
DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID)
------------------------------------
34
SQL> alter system dump datafile 23 block 34;
System altered.
在 udump 目录下查看 trace 文件的内容如下:
Start dump data blocks tsn: 34 file#: 23 minblk 34 maxblk 34
buffer tsn: 34 rdba: 0x 05c 00022 (23/34)
scn: 0x 0000.013943f 3 seq: 0x01 flg: 0x02 tail: 0x 43f 30601
frmt: 0x02 chkval: 0x0000 type: 0x06=trans data
Block header dump: 0x 05c 00022
Object id on Block? Y
seg/obj: 0x3ccd csc: 0x00.13943ef itc: 2 flg: O typ: 1 - DATA
fsl: 0 fnx: 0x0 ver: 0x01
Itl Xid Uba Flag Lck Scn/Fsc
0x01 0x 000a .02e.00000ad7 0x00800036.03de.18 --U- 1 fsc 0x 0000.013943f 3
0x02 0x0000.000.00000000 0x0000 0000.0000.00 ---- 0 fsc 0x0000.00000000
data_block_dump,data header at 0xadb 505c
===============
tsiz: 0x1fa0
hsiz: 0x14
pbl: 0x0adb 505c
bdba: 0x 05c 00022
76543210
flag=--------
ntab=1
nrow=1
frre=-1
fsbo=0x14
fseo=0x 1f 9a
avsp=0x 1f 83
tosp=0x 1f 83
0xe:pti[0] nrow=1 offs=0
0x12:pri[0] offs=0x 1f 9a
block_row_dump:
tab 0, row 0, @0x 1f 9a
tl: 6 fb: --H-FL-- lb: 0x1 cc: 1
col 0: [ 2] c1 02
end_of_block_dump
End dump data blocks tsn: 34 file#: 23 minblk 34 maxblk 34
对其中的一些信息做一些解释:
Fb:H 是指行记录的头, L 是指行记录的最后一列, F 是指行记录的第一列。
Cc :列的数量
Nrid :对于行链接或者行迁移来说的下一个 row id 的值
由上面的 dump 信息我们可以看出来当前表 test 是没有行链接或者行迁移的。
然后更新 test 表 , 并重新 dump 出来:
SQL> update test set a='test',b='test',c='test',d='test',e='test' where x=1;
1 row updated.
SQL> commit;
Commit complete.
此时应该有行迁移 / 行链接产生了。
SQL> alter system dump datafile 23 block 34;
System altered.
在 udump 目录下查看 trace 文件的内容如下:
Start dump data blocks tsn: 34 file#: 23 minblk 34 maxblk 34
buffer tsn: 34 rdba: 0x 05c 00022 (23/34)
scn: 0x0000.0139442b seq: 0x01 flg: 0x02 tail: 0x442b0601
frmt: 0x02 chkval: 0x0000 type: 0x06=trans data
Block header dump: 0x 05c 00022
Object id on Block? Y
seg/obj: 0x3ccd csc: 0x00.1394429 itc: 2 flg: - typ: 1 - DATA
fsl: 0 fnx: 0x0 ver: 0x01
Itl Xid Uba Flag Lck Scn/Fsc
0x01 0x 000a .02e.00000ad7 0x00800036.03de .18 C --- 0 scn 0x 0000.013943f 3
0x02 0x0004.002.00000ae0 0x0080003b.0441.11 --U- 1 fsc 0x0000.0139442b
data_block_dump,data header at 0xadb 505c
===============
tsiz: 0x1fa0
hsiz: 0x14
pbl: 0x0adb 505c
bdba: 0x 05c 00022
76543210
flag=--------
ntab=1
nrow=1
frre=-1
fsbo=0x14
fseo=0x 178a
avsp=0x 177c
tosp=0x 177c
0xe:pti[0] nrow=1 offs=0
0x12:pri[0] offs=0x 178a
block_row_dump:
tab 0, row 0, @0x 178a
tl: 2064 fb: --H-F--N lb: 0x2 cc: 3
nrid: 0x 05c 00023.0
col 0: [ 2] c1 02
col 1: [2000]
74 65 73 74 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
…………
col 2: [48]
74 65 73 74 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
end_of_block_dump
End dump data blocks tsn: 34 file#: 23 minblk 34 maxblk 34
我们不难看出, nrid 出现了值,指向了下一个 row id, 证明刚刚的 update 操作使这行记录产生了行链接或者行迁移了。
二、行迁移 / 行链接的检测
通过前面的介绍我们知道,行链接主要是由于数据库的 db_block_size 不够大,对于一些大的字段没法在一个 block 中存储下,从而产生了行链接。对于行链接我们除了增大 db_block_size 之外没有别的任何办法去避免,但是因为数据库建立后 db_block_size 是不可改变的(在 9i 之前),对于 Oracle9i 的数据库我们可以对不同的表空间指定不同的 db_block_size, 因此行链接的产生几乎是不可避免的,也没有太多可以调整的地方。行迁移则主要是由于更新表的时候,由于表的 pctfree 参数设置太小,导致 block 中没有足够的空间去容纳更新后的记录,从而产生了行迁移。对于行迁移来说就非常有调整的必要了,因为这个是可以调整和控制清除的。
如何检测数据库中存在有了行迁移和行链接呢?我们 可以利用 Oracle 数据库自身提供的脚本 utlchain.sql (在 $ORACLE_HOME/rdbms/admin 目录下) 生成 chained_rows 表,然后利用 ANALYZE TABLE table_name LIST CHAINED ROWS INTO chained_rows 命令逐个分析表,将分析的结果存入 chained_rows 表中。 从 utlchain.sql 脚本中我们看到 chained_rows 的建表脚本,对于分区表, cluster 表都是适用的。然后可以使用拼凑语句的办法生成分析所需要的表的脚本,并执行脚本将 具体的分析数据放入 Chained_rows 表中, 例如下面是分析一个用户下所有表的脚本:
SPOOL list_migation_rows.sql
SET ECHO OFF
SET HEADING OFF
SELECT 'ANALYZE TABLE ' || table_name || ' LIST CHAINED ROWS INTO chained_rows;' FROM user_tables;
SPOOL OFF
然后查询 chained_rows 表,可以具体查看某张表上有多少的行链接和行迁移。
SELECT table_name, count(*) from chained_rows GROUP BY table_name;
当然,也可以查询 v$sysstat 视图中的 ’table fetch continued row’ 列得到当前的行链接和行迁移数量。
SELECT name, value FROM v$sysstat WHERE name = 'table fetch continued row';
可以使用如下的脚本来直接查找存在有行链接和行迁移的表,自动完成所有的分析和统计。
accept owner prompt " Enter the schema name to check for Row Chaining (RETURN for All): "
prompt
prompt
accept table prompt " Enter the table name to check (RETURN for All tables owned by &owner): "
prompt
prompt
set head off serverout on term on feed off veri off echo off
!clear
prompt
declare
v_owner varchar2(30);
v_table varchar2(30);
v_chains number;
v_rows number;
v_count number := 0;
sql_stmt varchar2(100);
dynamicCursor INTEGER;
dummy INTEGER;
cursor chains is
select count() from chained_rows;
cursor analyze is
select owner, table_name
from sys.dba_tables
where owner like upper('%&owner%')
and table_name like upper('%&table%')
order by table_name;
begin
dbms_output.enable(64000);
open analyze;
fetch analyze into v_owner, v_table;
while analyze%FOUND loop
dynamicCursor := dbms_sql.open_cursor;
sql_stmt := 'analyze table '||v_owner||'.'||v_table||' list chained rows into chained_rows';
dbms_sql.parse(dynamicCursor, sql_stmt, dbms_sql.native);
dummy := dbms_sql.execute(dynamicCursor);
dbms_sql.close_cursor(dynamicCursor);
open chains;
fetch chains into v_chains;
if (v_chains != 0) then
if (v_count = 0) then
dbms_output.put_line(CHR(9)||CHR(9)||CHR(9)||'<<<<< Chained Rows Found >>>>>');
v_count := 1;
end if;
dynamicCursor := dbms_sql.open_cursor;
sql_stmt := 'Select count() v_rows'||' From '||v_owner||'.'||v_table;
dbms_sql.parse(dynamicCursor, sql_stmt, dbms_sql.native);
dbms_sql.DEFIN