Oracle内核技术揭密. 1.2 段中块的使用

1.2 段中块的使用

在讲解本节主题前,我们先来理清一个概念,什么是段。在Oracle中,表和段是两个截然不同的概念。表从逻辑上说明表的形式,比如表有几列,每列的类型、长度,这些信息都属于表。而段只代表存储空间,比如,上节中提到的区,就是属于段。一个段中至少要包含一个区。

Oracle中,每个对象都有一个ID值,表有表的ID,段有段的ID。在DBA_OBJECTS数据字典视图中,object_id列是表ID,data_object_id列是段ID,下面查看了某个表的表ID和段ID:

SQL> create table lhb.table_lhb2 (id int,name varchar2(20)) tablespace tbs_ts2;

Table created.

 

SQL> select object_id,data_object_id from dba_objects where owner='LHB' and

 object_name='TABLE_LHB2';

 

 OBJECT_ID DATA_OBJECT_ID

---------- --------------

 13039     13039

从上面信息可知,这里创建了一个表TABLE_LHB2,初始情况下,它的表ID和段ID是一样的,都是13039。

表ID一旦创建,就不会再改变。但段ID是会变化的,比如,当Truncate表时,Oracle会将表原来的段删除,再为表新建一个段。也就是将表原来的存储空间释放,再重新分配新的区。这个过程完毕后,表就换了一个段,所以,表ID不变,但段ID却变了。如下所示:

SQL> insert into lhb.table_lhb2 values(1,'abc');

1 row created.

 

SQL> commit;

Commit complete.

 

SQL> truncate table lhb.table_lhb2;

Table truncated.

 

SQL> select object_id,data_object_id from dba_objects where owner='LHB' and

object_name='TABLE_LHB2';

 

 OBJECT_ID DATA_OBJECT_ID

---------- --------------

 13039     13040

可以看到,在Truncate表后,OBJECT_ID不变,DATA_OBJECT_ID变了。基本上,每Truncate一次,段ID都会加1。

注意,上面的测试是在Oracle 11GR2中做的,如果是在Oracle 10g中,创建表后不需要插入一行,直接Truncate,就可以观察到段ID的变化。

1.2.1 块中空间的使用

一个块的大小最常见是8KB。对于这8KB空间的使用,网上已经有很多描述,这里简单说一下。块中信息分两部分:管理信息和用户数据,其中,管理信息包括块头的SCN、ITL槽等。

块的结构相信很多人也研究过,下面讨论一个常见问题:如果删除了一行,再回滚,行的位置会变吗?

测试如下:

SQL> select dbms_rowid.ROWID_RELATIVE_FNO(rowid) fno,dbms_rowid.rowid_block_

number(rowid) block_id,dbms_rowid.ROWID_ROW_NUMBER(rowid) row_id,id from lhb.

table_lhb2;

       FNO   BLOCK_ID     ROW_ID         ID

---------- ---------- ---------- ----------

         5        517          0          1

         5        517          12<---(删除此行再回滚)

         5        517          23

         5        517          34

这里使用了一个包,dbms_rowid,它的作用是从ROWID中将对象ID、文件号、块号、行号分解出来。或者把对象ID、文件号、块号、行号合并成ROWID,具体使用方法这里不再列出,可以参考Oracle官方文档PL/SQL Reference,其中有详细的说明。这里,使用它的第一种功能,从ROWID中解析出块号、行号等信息。如果向lhb.table_lhb2表中依次插入ID为1、2、3、4的4行数据,观察ROW_ID列,可以看到,这4行的行编号分别是0、1、2、3。

下面将ID为2的行(行编号是1)删除,再回滚,然后再次查看。

SQL> delete lhb.table_lhb2 where id=2;

1 row deleted.

SQL> rollback;

Rollback complete.

 

SQL> select dbms_rowid.ROWID_RELATIVE_FNO(rowid) fno,dbms_rowid.rowid_block_

number(rowid) block_id,dbms_rowid.ROWID_ROW_NUMBER(rowid) row_id,id from lhb.

table_lhb2;

 

       FNO   BLOCK_ID     ROW_ID         ID

---------- ---------- ---------- ----------

         5        517          0          1

         5        517          12<---(回滚后行号不变)

         5        517          23

         5        517          34

结果不变,ID为2的行,还是在行号为1的位置。

道理很简单,删除某行,其实只是在行上加个删除标志,声明此行所占的空间可以被覆盖。在没有提交时,事务加在行上的锁并没有释放,此行虽然已经打上了删除标志,但空间仍不会被其他行覆盖。而删除行的回滚,其实就是将被删除的行重新插入一次。但回滚时的插入和普通插入一行还是有一定区别的。因为被删除行的空间不会被覆盖,所以回滚时的插入,不需进行寻找空间的操作,而是行原来在哪儿,就还插入到那里。这也就是它和普通插入的区别。

因此,删除的回滚,不会改变行原来的位置。

但如果删除后提交再插入呢?行的位置肯定就会发生变化了。

1.2.2 典型问题:堆表是有序的吗

曾经有位开发人员跟我聊到,他曾做过测试,插入几万行,删掉,再插入,发现原来Oracle中堆表是按插入顺序安排行的位置的,而且这个测试他做了好多遍,都是这个结果。现在他们有个应用,显示数据时,要求先插入的行在前,后插入的行在后,其实Oracle已经帮他们实现了这个功能。

事实上,堆表是无序的,堆表的特点就是无序、插入快速。

Oracle在插入行时是如何在数据块内查找可用空间的呢?这有点类似于上节中提到的区的分配过程。Oracle会在数据块中设立一个标记位,记录空间使用到哪儿了。

块中用户数据所占空间是从下往上分配的。假设,在8192字节的块中插入了5行,每行100字节,也就是说,空间已经使用到了(8192-500)7692字节处,那么,标记位的值就是7692。

如果删除了其中一行并提交,标记位的值不会变,还是7692。再重新插入被删除行,或插入新行,将会从7692处向上查找可用空间,删除行释放出的空间不会被使用。

当标记位的值越来越小,向上到达管理性信息的边界时,标记位会再变为8192。

我们可以测试一下。

SQL> delete lhb.table_lhb2 where id=2;

1 row deleted.

 

SQL>commit;

Commit complete.

 

SQL> insert into lhb.table_lhb2 values (2,'ABC');

1 row created.

 

SQL> commit;

Commit complete.

 

SQL> select dbms_rowid.ROWID_RELATIVE_FNO(rowid) fno,dbms_rowid.rowid_block_

number(rowid) block_id,dbms_rowid.ROWID_ROW_NUMBER(rowid) row_id,id from lhb.

table_lhb2;

       FNO   BLOCK_ID     ROW_ID         ID

---------- ---------- ---------- ----------

         5        517          0          1

         5        517          2          3

         5        517          3          4

         5        517          4          2<------删除提交后再插入,

                                                        被分配到了新的位置

在上面的测试中,先删除ID为2的行,提交后接着又插入ID为2的行。不过,新插入的行并没有使用刚刚删除行的空间。

如果只测试到这一步,很容易得出结论,行的位置就是插入顺序。但别忘了,我们只在一个块内进行了观察,查找了可用空间。在众多的块中,Oracle是如何选择要向哪个块中插入的呢?情况会不会有变化呢?我们还不知道。

所以,现在还不能完全回答“堆表是有序的吗”这个问题,继续向下看,据说ASSM对插入的影响是巨大的,那接下来看看ASSM。

1.2.3 ASSM与L3、L2、L1块的意义

ASSM的目的是大并发插入,这应该是DBA要掌握的基本知识。在输入输出能力满足的情况下,使用ASSM就能有大并发插入吗?这可不一定。工具再好,还要看我们如何使用工具。

在了解ASSM的使用注意事项之前,先来分析一下ASSM。为什么Oracle对外宣称ASSM可以支撑大并发插入应用呢?

ASSM的整体结构是3层位图块+数据块,即共4层的树状结构。

第一层位图块称为L3块,一个L3块中可以存放多个L2块的地址,一个L2块中可以存放多个L1块地址,一个L1块中可以存放多个数据块地址,如图1-2所示。

 

图1-2 ASSM的整体结构

第一个L3块一般是段头。如果段头中存放了太多L2块的信息,空间不足,Oracle会再分配第二个L3块。当然,段头中会有第二个L3块的地址。如果第二个L3块空间也用完了,会再分配第三个。第二个L3块中会存放第三个L3块的地址。通常情况下,一个L3块就够了。有两个L3块就已经是非常罕有的情况了,基本上不会出现需要3个L3块的情况。

Oracle是如何使用4层树状结构(3层位图块+数据块)来确定向哪个块中插入的呢?

第一步,查找数据字典(就是dba_segments数据字典视图的基表),确定段头位置。

第二步,在段头中找到第一个L2块位置信息。

第三步,到L2块中根据执行插入操作进程的PID号,做HASH运算,得到一个随机数N,在L2中,找到第N个L1块的位置信息。

第四步,到第三步中确定的L1块中,再根据执行插入操作进程的PID号,做HASH运算,得到一个随机数M,在L1中找到第M号数据块。

第五步,向第M号数据块中插入。

L3块中虽然可以有多个L2块,但插入操作不会选择多个L2块,每次只会选择同一个L2块。直到这个L2块下面的所有数据块都被插满了,才会选择下一个L2块。

在L2中选择某个L1的时候,就是随机的了。不同Session,只要有可能,就会被分配到不同的L1中。在L1中找数据块时也是一样。

现在我们可以回答这个问题了:Oracle为什么宣称ASSM可以支持大并发插入。

假设一个L2中有100个L1,每个L1中有64个数据块,可以算一下,100×64,如果Oracle的随机算法真的够随机,如果有6400个进程一起执行插入操作,Oracle会随机地将它们分配到6400个数据块中。

Oracle的随机算法一向都是值得信赖的。

所以,在Oracle的所有资料中,都宣称ASSM可以支撑大并发插入。

但实际情况往往不像想象中的这么简单。

1.2.4 值得注意的案例:ASSM真的能提高插入并发量吗

这个案例很有代表性,如果不深入到细节中,很容易在中途得出错误的结论。下面详细描述思考过程,希望能给大家带来些启发。

曾经遇到过这样的应用,要求对用户的登录、退出行为做记录。此部分的逻辑很简单,用户每登录一次应用,向数据库中一个日志表中插入一行,退出应用的时候再向日志表中插入一行。

此日志表是个日分区表,每天一个分区。每天大约会插入千万行,除了插入并发很高以外,就没有其他的大并发操作。另外,每天晚上会将当天的数据推送到数据仓库,在数据仓库中再进行分析、对比。

项目上线后,有些用户反映登录变慢了。而且,只有上午八九点钟左右的时候慢,过了这一段时间就没有用户反映有问题。经过对比AWR,发现变慢是不定时的,从8点开始,到9点左右为止,在半小时一次的报告中,偶尔会有那么一两份AWR会显示Buffer Busy Waits比较高,然后就正常了。

看到这个情况,很容易让人认为是某个时间段有很多人一起在访问同一张表,其他时间又不一起访问了。究竟是不是这么回事呢?

先来确定一下等待是针对哪个对象。通过V$SEGMENT_STATISTICS,查找STATISTIC_NAME列为buffer busy waits的,或者,查看V$ACTIVE_SESSION_HISTORY中的历史等待事件,根据P1、P2列的值,就可以定位争用是针对哪个对象的。

根据文件号、块号查找的结果来看,绝大多数的Buffer Busy Waits都出现在日志表上。

日志表每天分区的数据量最高接近千万行,就按每天1000万行算,除以3600×24,平均每秒116个并发插入。当然,还要考虑高低峰的问题,晚上应用基本上没什么人用的,这几百万行大部分都是白天插入的。所以,再乘个2,每秒232的插入量,这是最高的了。也并不是很多,这点量和Oracle宣称的ASSM支持的高并发插入相比,应该不会有Buffer Busy Waits。

但无论如何,Buffer Busy Waits是产生了,有可能以主机的硬件来论,现在已经是并发插入量的极限了。但奇怪的是,这种情况每天只会在刚上班后不久(8~9点)出现,其他时段正常。

难道是刚上班时向日志表的插入量高?

但统计的结果显示,白天有好几个时段,日志表的插入量都很大,并不是早上上班时段特别大,有时下午还会比上午插入的稍多些,但没有发现下午日志表上有Buffer Busy Waits,下午也从来没人反映过慢,而且整库的压力上下午基本差不多。

如果全天都有Buffer Busy Waits,我想我也会放弃进一步调查。但有时下午的插入量多,反而没有等待。那说明ASSM是足以支撑这个量级的并发插入的。想解决问题的话,第一步是定位问题,这我们都知道。可如何定位这个问题呢?

遇到这样的疑难杂症,一般的方法是在测试环境中详细地分析相关操作,甚至可以使用DTrace加MDB/GDB这种底层分析工具。总之,只有清楚地了解底层操作,才能分析出问题在哪儿。

如何发现现在遇到的这个问题出在哪儿呢?

很简单,还是从最基本的测试做起。先建一个表,验证一下Oracle插入时,是否会随机地选择块。如下所示:

SQL> drop tablespace tbs_ts1 INCLUDING CONTENTS;

 

Tablespace dropped.

 

SQL> create tablespace tbs_ts1 datafile '/u01/Disk1/tbs_ts1_01.dbf' size 50m reuse

uniform size 1m;

 

Tablespace created.

 

SQL> create table table1(id int,name varchar2(20)) tablespace tbs_ts1;

 

Table created.

由于线上环境表空间区大小是1MB,因此在测试环境,我也创建了个区大小为1MB的表空间。

在Oracle 10g以后,Oracle默认的表空间类型就是ASSM了,所以,不需要专门指定了。

接着,在tbs_ts1表空间中创建一个测试表TABLE1,下面来看看它的区占用情况。

SQL> select extent_id, file_id, block_id, blocks from dba_extents where

owner='LHB' and segment_name='TABLE1' order by extent_id;

 

 EXTENT_ID    FILE_ID   BLOCK_ID     BLOCKS

---------- ---------- ---------- ----------

         0          4        128        128

可以看到,TABLE1在4号文件中,第一个区开始自128号块处。可以DUMP一下128号块看看,它是一个L1块。129号块也是一个L1块,130号块是L2块,131号块是段头,也是L3块。

128号和129号块中,各自有64个数据块信息。这一点,可以通过DUMP来确认。

下面,插入一行,试试看这一行将被插入哪个块中。

SQL> insert into table1 values(1,'AAAAAA');

 

1 row created.

 

SQL> commit;

 

Commit complete.

SQL> select dbms_rowid.ROWID_RELATIVE_FNO(rowid) fno,dbms_rowid.rowid_block_

number(rowid) block_id,dbms_rowid.ROWID_ROW_NUMBER(rowid) row_id,id from lhb.table1;

 

       FNO   BLOCK_ID     ROW_ID         ID

---------- ---------- ---------- ----------

         4        155          0          1

在插入这一行并提交后,可以用之前介绍过的语句,查看这一行的位置。可以看到,它被插入在了4号文件155号块中。换个会话再插入一行试试。

SQL> insert into table1 values(2,'BBBBBB');

 

1 row created.

 

SQL> commit;

 

Commit complete.

 

SQL> select dbms_rowid.ROWID_RELATIVE_FNO(rowid) fno,dbms_rowid.rowid_block_

number(rowid) block_id,dbms_rowid.ROWID_ROW_NUMBER(rowid) row_id,id from lhb.table1;

 

       FNO   BLOCK_ID     ROW_ID         ID

---------- ---------- ---------- ----------

         4        155          0          1

         4        156          0          2

在另一个会话中,插入了ID为2的行,它被插入在了156号块中。

不同的会话,Oracle会将行插入到不同块中。Oracle是根据PID计算出的随机数,随机地将行插入在不同的块中。只要PID不一样,行就会被插入在不同的块中。在PID一样的情况下,行会被插入在同一块中。

比如,在第一个会话中再插入一个ID为3的行。

SQL> insert into table1 values(3,'AAAAAA');

 

1 row created.

 

SQL> commit;

 

Commit complete.

 

SQL> select dbms_rowid.ROWID_RELATIVE_FNO(rowid) fno,dbms_rowid.rowid_block_

number(rowid) block_id,dbms_rowid.ROWID_ROW_NUMBER(rowid) row_id,id from lhb.table1;

 

       FNO   BLOCK_ID     ROW_ID         ID

---------- ---------- ---------- ----------

         4        155          0          1

         4        155          1          3

         4        156          0          2

ID为3的行也被插入到155号块中。因为它和ID为1的行是在同一会话中插入的,会话对应进程的PID相同,两行就被插入了同一个块中。

另外,我们可以发现,后插入的ID为3的行,在显示时被排在先插入的ID为2的行前了。这说明堆表中行的排列也并非是插入顺序。

现在我们终于可以对前面提出的“堆表是有序的吗”问题给出一个明确的回复了。那就是:完全无序。因为插入时有个根据PID计算随机数的过程,这就会导致行被插入哪个块是随机的。因此,堆表是无序的。

继续观察行被插入的位置。但如果我们老是通过sqlplus lhb/a建立一个会话,在会话中插入,这样太麻烦了,还是写个脚本吧。

$ cat assm_test.sh

sqlplus lhb/a <<EOF

insert into lhb.table1 values($1,'aaabbbcccd');

commit;

exec dbms_lock.sleep(10000);

EOF

关于Shell脚本的编写,这里不再解释。下面只说一点,为什么最后要加如下语句:

exec dbms_lock.sleep(10000);

如果没有这个暂停操作,会话将立即结束。在Oracle中,如果前一个会话结束,下一个会话马上建立,则下一个会话将会有和前一个会话相同的Session ID和PID(注意,PID不是SPID,PID是Oracle对进程的编号)。如果两个会话的PID相同,行将被插入在同一块中。所以,这里专门加个“暂停”操作,让会话停10000秒后再退出。这样,再新建一个会话,它将有一个新的PID。

按如下方法,将上述脚本执行10次:

./assm_test.sh 4&

./assm_test.sh 5&

加一个&,表示放在后台执行,要不然要等10000秒才能结束。

查看一下这些行都被插到哪儿了。

SQL> select dbms_rowid.ROWID_RELATIVE_FNO(rowid) fno,dbms_rowid.rowid_block_number

(rowid) block_id,dbms_rowid.ROWID_ROW_NUMBER(rowid) row_id,id from lhb.table1;

 

       FNO   BLOCK_ID     ROW_ID         ID

---------- ---------- ---------- ----------

         4        155          0          1

         4        155          1          3

         4        156          0          2

         4        157          0          4

         4        158          0          5

         4        159          0          6

         4        160          0          8

         4        161          0          7

         4        162          0          9

         4        163          0         10

         4        164          0         11

         4        165          0         13

         4        166          0         12

 

13 rows selected.

还是很平均的,每个块一行。我们看一下ROW_ID列,这是行在块中的行号。除了刚才做测试的ID为3的行,其他行都是块中的第一行(行号为0)。

平均是很平均,但我们应该也注意到了一个问题,在后面所做的10次插入,虽然这10行的确被插到了10个块中,但是,这些块未免有点太集中了。

table1表现在共有128个块,块编号从128到255。但这些行都被插到了155~166号块中。

这应该是Oracle的算法不够随机吧。

一开始我觉得,是区不够多,只有一个区,128个块,Oracle选择面太窄了。我们知道,表在扩展时,也都是一个区一个区地扩展的。每次占满了128个块后,再扩展下一个区。但下一个区也还是128块,还是只在128个块中选择。由于随机算法不够随机,导致在128选一时,很多行被同时插到了同一个块中,这时,就会出现Buffer Busy Waits。

一切都是合乎情理,我马上将发现告知应用方。解决方案就是,在晚上数据库空闲时,为日志表手动分很多个区。

第二天,客户依然反映,运行速度慢。查看数据库,还是有Buffer Busy Waits。

为什么?

看来是第一次的实验做得不够彻底。为什么使用的块是155号、156号、157号等,这么有顺序,而且不够分散呢?

继续前面的测试。这次,我调用./assm_test.sh N&,每10次观察一下行的分配情况,终于发现了问题。

SQL> select dbms_rowid.ROWID_RELATIVE_FNO(rowid) fno,dbms_rowid.rowid_block_number

(rowid) block_id,dbms_rowid.ROWID_ROW_NUMBER(rowid) row_id,id from lhb.table1;

 

 

       FNO   BLOCK_ID     ROW_ID         ID

---------- ---------- ---------- ----------

         4        132          0         39

                   ……………………

         4        159          0          6

         4        159          1         67

         4        160          0          8

         4        160          1         66

         4        161          0          7

         4        161          1         68

         4        162          0          9

         4        162          1         69

                   ……………………

         4        190          0         38

         4        191          0         37

 

69 rows selected.

一共插入了69行,最小的块号是132。这个可以理解,因为表的第一个块编号是128,128号、129号块是L1,130号是L2,131号是段头兼L3。第一个可用数据块是从132开始的。但是到150号块后,就开始有重复,两行被插入同一块中。还有一点就是,最大使用的块是191号。用192-128,正好等于64。

继续分析下去有个关键点,要看之前对细节的挖掘程度了。前面我们一再地提过,对于1MB大小的区,每个区最前面的两个块,大多数情况下是L1块。在8KB的块大小下,1MB共128个块,两个L1,正好每个L1记录64个数据块。

好了,答案基本上已经浮出水面。

Oracle只使用了第一个L1块中的数据块,而没有使用第二个L1中的块。

其实还有一个知识点,如果不具备,可能分析就到这里为止了。前面也提过了,Oracle在L3、L2、L1、数据块中这个树状图中选择要插入的块时,从L3中选择L2并不是随机的,每次都只选某一个。但从L2中选择L1是随机的。关于这一点,我已经做了测试。

现在L2中有两个L1,会什么Oracle只选择第一个L1呢?

你想到原因了吗?

我是这样想到原因的,我曾经做过直接路径插入的测试,这个测试验证了如果进行直接路径插入,每次会在高水点之上分配空间,如果提交,则修改高水点。如果不提交,则不修改高水点,通过这种方式可减少UNDO的耗用。而普通的插入则是在高水点之下寻找空间。

我们一直没有提过高水点。直接路径都是在高水点之上插入的,那么间接路径呢?肯定是在高水点之下了。

好,答案已经见分晓了。高水点肯定在第192号块。因为第二个L1块中的数据块,都在高水点之上,因此,第二个L1块中的数据块不会被插入算法选择到。

DUMP一下段头验证一下吧。

  Extent Control Header

---------------------------------------------------------------

Extent Header:: spare1: 0      spare2: 0     #extents: 1      #blocks: 128  

last map        0x00000000     #maps: 0      offset: 2716 

Highwater::     0x010000c0      ext#: 0      blk#: 64         ext size: 128 

代码中加下划线的就是高水点了。0x010000c0,这个是DBA(Data Block Address,数据块地址)。它的前10个二进制位是文件号,后面的是块号。0x010000c0也就是4号文件192号块。

看来Oracle的高水点每次向后移动时,是以L1块中的数据块数量为单位的啊。

水落石出了,原来是高水点太低的问题。

Oracle只告诉我们,ASSM可以增大插入并发量,但没告诉我们,并发插入量还要受高水点限制。

以前曾经有人讨论过在MSSM表空间中高水点的移动规则,而ASSM下高水点的推移规则还很少有人注意过。

当在区中插入第一行时,高水点移到区的第一个L1块中最大的数据块后。这句话有点绕,还是以我们的测试表TABLE1为例吧:插入第一行时,高水点移到了第一个L1块(128号块)中最大的数据块后,128号块中最大的数据块是191,那么高水点就是192了,其实也就是第二个L1块中的第一个数据块。

简单总结一下,高水点的移动,在ASSM下,是以L1中数据块的数量为准的。

如果块大小是8KB,区大小是1MB,L1中有64个数据块,高水点就是以64个块为单位,依次往后挪的。也就是说,我们的并发插入,每次都只是向64个块中插入。可以想象,如果同时有100个进程插入,但只有64个块接收,将有36个进程不得不和另一个进程同时向一个块中插入。

两个进程同时修改一个块,会有什么等待时间呢?Buffer Busy Waits(当然也会有少量的Cache Buffer Chain Latch)。

问题已经找到一大半了,ASSM表空间仍有可能因为高水点不高,可用于插入的块不多,造成Buffer Busy Waits。但另一半问题隐藏得更深,为什么只会在刚上班那会儿出现这个等待,而其他时间则没有呢?

注意,白天的时候,压力是差不多的。有时下午比上午还要高。

要解答这个问题,就看你对Oracle的内部机制有多大的好奇心了。

我挖掘出这个问题纯属意外。

其实在发现了高水点问题后,我建议使用抬高高水点的方式解决争用问题。

当然,抬高高水点后,将对全表扫描不利。全表扫描只扫描高水点之下的块,如果高水点太高,要扫描的块也多了。

但这个日志型应用,平常没有全表扫描,只有在每天晚上向数据仓库传数据时,需要全表扫描。因此,对全表扫描的影响不是主要考虑的因素。

如何抬高呢?手动分配区是无法抬高高水点的。只有一种方法,先插入行再删除。

因为日志表是一个日分区表,按照日期,每天一个分区。考虑到每天的插入量不会高于1000万行,因此决定对未来的每个分区,先插入1000万行,再用Delete删除。

具体的方案是这样的,先使用APPEND向一张中间表中插入1000万行,采用直接路径方式,这样产生的UNDO量较少。再用Delete慢慢删除,根据ROWID来删除,一次删除一个区的所有行,然后提交。将整个表删除完后,高水点就已经被抬高了,但表中是没有行的。再使用分区交换命令,将被抬高高水点的中间表交换到日志表中。

这种方法听起来有点不太规范,但没办法,暂时只能这样解决了。

实事上,我用上面的方式调高了几个分区的高水点,第二天观察,果然在全天任意时候,都不再有Buffer Busy Waits了。

其实如此交差也可以,就是加分区的时候麻烦点。若用脚本实现,只是在Delete的时候慢点,不占太多回滚段就不会有任何问题。

但还有一个问题一直困扰着我,但这个问题和应用已经无关了,我只是好奇:一个L1中有64个数据块,64这个数字是固定的吗?

我分别用40KB(5个8K的块,已经是Oracle中最小的区了)、1MB、10MB、30MB大小的区测试,40KB的区中,一个L1中可以只有5个数据块,是最少的。但1MB、10MB、30MB的区,都是一个L1中有64个块。64个块应该就是L1中数据块的最大数量了。

Oracle的系统管理区大小是随着段的不断变大而不断变大的,L1会不会也是这样呢?我决定再试一下。

用手动分配区的命令,为TABLE1多分配些区。我为TABLE1每次分配30MB空间,每次DUMP一下最后一个区的第一个块(每个区第一个块通常都是L1块)。

当分配的总空间到90MB时,我发现L1中的数据块数量从64增加到了256个。测试如下:

SQL> drop tablespace tbs_ts1 INCLUDING CONTENTS;

 

Tablespace dropped.

 

SQL> create tablespace tbs_ts1 datafile '/u01/Disk1/tbs_ts1_01.dbf' size 100m

 =reuse uniform size 1m;

 

Tablespace created.

 

SQL> create table table1(id int,name varchar2(20)) tablespace tbs_ts1;

 

Table created.

 

SQL> alter table table1 allocate extent (size 90m);

 

Table altered.

 

SQL> set pagesize 1000

SQL> select extent_id, file_id, block_id, blocks from dba_extents where

owner='LHB' and segment_name='TABLE1' order by extent_id;

 

 EXTENT_ID    FILE_ID   BLOCK_ID     BLOCKS

---------- ---------- ---------- ----------

         0          4        128        128

         1          4        256        128

         2          4        384        128

…………………………

        88          4      11392        128

        89          4      11520        128

        90          4      11648        128

 

91 rows selected.

上面删除了表空间,重新建了个全空的,区大小1MB。又创建了个新表,TABLE1,手动分配90MB空间。它一共有91个区。

分别DUMP一下第128号块和11520号块。以下是4号文件128号块的DUMP结果:

mapblk  0x00000000  offset: 0    

  --------------------------------------------------------

  DBA Ranges :

  --------------------------------------------------------

0x01000080  Length: 64     Offset: 0     

 

   0:Metadata        1:Metadata       2:Metadata       3:Metadata

   4:unformatted     5:unformatted    6:unformatted    7:unformatted

   8:unformatted     9:unformatted   10:unformatted     ………………

         ……………………

   60:unformatted   61:unformatted   62:unformatted   63:unformatted

  --------------------------------------------------------

可以看到,这个L1*有64个数据块。以下是11520号块的DUMP结果:

  --------------------------------------------------------

  DBA Ranges :

  --------------------------------------------------------

0x01002d00  Length: 128    Offset: 0     

0x01002d80  Length: 128    Offset: 128

 

   0:Metadata          1:unformatted     2:unformatted     3:unformatted

   4:unformatted       5:unformatted     6:unformatted     7:unformatted

…………………………

   252:unformatted   253:unformatted   254:unformatted   255:unformatted

  --------------------------------------------------------

在这个L1块中,数据块的数量增加到了256个。

这证明了L1块中记录的数据块个数也是随着表的不断增大而增多的。

这个证明有何意义呢?还记得上面遇到的问题吧,每天总是在刚上班时会有Buffer Busy Waits,而其他时间则没有。现在有答案了。

因为日志表每天一个分区,也就是每天一个段。刚上班时,段还比较小,L1块中只有64个数据块,因此并发插入每次都只针对64个块。随着表增大,当表超过90MB时,一个L1就有256个数据块了,即使所有并发都只针对一个L1中的数据块,256个块也足以支撑这套应用的所有并发了。因此,每天总是在最开始不长一段的时间内,会有Buffer Busy Waits,再往后就正常了。

这个奇怪的问题终于找到了原因。其实我研究L1中数据块的数量,本来只是为了满足好奇心,没想到可以查找出这个问题的原因。

更进一步,可以再试一下不同区大小、不同段大小下,L1块中数据块的数量。

我测试的结果是,10MB区大小,从第4个区开始,L1块中数据块的数量就已经是256个了。10MB的区好像有点大了,我只测试了一下4MB或8MB的区,在段大小超过64MB后,L1块中数据块的数量会达到256个。

好,研究得差不多了。可问题该如何解决呢?方法还和刚才一样,先插入,再删除。只不过,不需要插入1000万行了。我选择建立8MB区大小的表空间,日志表新的分区都建到新表空间中。每个分区只需插入50万行再删除就可以了。

只需要将前8个分区,插入满行,再删除,将高水点推到第8个分区后,因为第8个分区后,每个L1块中都是256个数据块,足够支撑并发插入量了。

该问题终于有了一个比较好的解决方案。但后面经过观察又发现,在L1块上出现了争用,但不严重,没有造成反应延迟。Oracle的高水点每次以L1块中数据块的量为单位向后扩,始络会有问题。如果同一时刻的并发超过了256个,一样会有争用,而且,这么大的量,L1块的竞争也会大大加剧。这样的话,解决方法只有一个,就是像我最初的方案一样,插入很多行(比如1000万行),将高水点拉得很高,再删除。

好了,ASSM的问题就说到这儿。看来随便建个ASSM表空间,再建个表上去,就想支撑大并发插入,这种想法有点简单了。

实际案例就先介绍到这儿。希望通过这个案例读者能有所收获。

补充一句:对Oracle越熟悉,面临的疑难杂症就越少。

关于表空间和存储结构,还有两个疑问:全表扫描时,Oracle是如何找到表的块在哪儿的?索引扫描Oracle是如何找到Root块的?

1.2.5 段头与Extent Map

上一节提到了,段头是第一个L3块,就是说段头中包含L3信息。其实,段头中的重要信息,除了L3外,还有Extent Map,将其直译过来就是区地图。

顾名思义,区地图就是记录一个段中所有区都在哪儿的地图。全表扫描操作,就是按图索骥,按区地图逐个读取所有区。

让我们来看看区地图是什么样子,同时,也模拟一下全表扫描的执行流程。

第一步,确定段头位置。

SQL> select header_file ,header_block from dba_segments where segment_name='TABLE1';

HEADER_FILE HEADER_BLOCK

----------- ------------

          4          131

当然,Oracle肯定不会读dba_segments这个数据字典视图,它会读dba_segments低层seg$这样的数据字典表。会先到共享池中的字典缓存中查找seg$相关的行,如果没有找到,再到Buffer Cache中读seg$相关的块,如果还没有,就到磁盘上SYSTEM表空间中读seg$表。

当找到TABLE1的段头位置时,Oracle会读取它里面的区地图,我们来DUMP一下。

执行下面的命令DUMP:

exit

sqlplus / as sysdba

alter system dump datafile 4 block 131;

就是先退出sqlplus,再重新连接,然后去DUMP。因为同一服务器进程会把DUMP信息写到一个DUMP文件中。如果你DUMP多次,被会写进一个文件,这样观察起来不方便。我退出再登录,服务器进程会换一个的,SPID也会不同,这样DUMP信息会被写到不同的文件中,便于查看。

下面就是段头中的区地图信息:

  Extent Map

  -----------------------------------------------------------------

0x01000080  length: 128  

0x01000100  length: 128  

0x01000180  length: 128  

…………

0x01002d00  length: 128  

0x01002d80  length: 128

第一个区,开始自0x01000080处,前10个二进制位是文件号,后面是块号,前面已经提到过的,也就是4号文件128号块处。这个区的大小是128个块,最后一个区,开始自4号文件11648号块处(就是最后一行0x01002d80),大小也是128个块。

我们已经看到区地图了,很简单是吧?但全表扫描时Oracle读取的并不是这里的区地图,还要往下看。

Auxillary Map

--------------------------------------------------------

 Extent 0     :  L1 dba:  0x01000080 Data dba:  0x01000084

 Extent 1     :  L1 dba:  0x01000100 Data dba:  0x01000102

 Extent 2     :  L1 dba:  0x01000180 Data dba:  0x01000182

                   ………………

 Extent 89    :  L1 dba:  0x01002d00 Data dba:  0x01002d01

 Extent 90    :  L1 dba:  0x01002d00 Data dba:  0x01002d80

--------------------------------------------------------

在上面的信息中,出现了Auxillary Map,直译过来是辅助地图。这一部分信息更详细。L1 dba:  0x01000080,说明了此区内第一个L1块开始的地方,即4号文件的128号块。Data dba:  0x01000084,说明用户数据开始的地方,即132号块。这里说明了真正的用户数据开始自哪里,Oracle全扫描时,是按照“Data dba:*******”后的DBA查找区的。但这里没有区长度,所以,上面那部分区地图信息还是要读的。

另外,我们看最后两行:

Extent 89    :  L1 dba:  0x01002d00 Data dba:  0x01002d01

Extent 90    :  L1 dba:  0x01002d00 Data dba:  0x01002d80

这两行的L1 Dba一样,都是0x01002d00,即4号文件11520号块。为什么这样?因为11520号块中有256个数据块,所以这两个区只需要有一个L1块就行了。可以观察一下从什么地方开始两个区只要一个L1块,这里是从8192号块开始的。

Extent 61    :  L1 dba:  0x01001f00 Data dba:  0x01001f02

Extent 62    :  L1 dba:  0x01001f80 Data dba:  0x01001f82

Extent 63    :  L1 dba:  0x01002000 Data dba:  0x01002001

Extent 64    :  L1 dba:  0x01002000 Data dba:  0x01002080

可以看到,61号、62号区,还各自有不同的L1号块,而63号、64号区,已经只有63区头的一个L1块了。63号区也就是第64个区,每个区1MB,也就是当段大小超过64MB时,一个L1将放存256个数据块。

好了,这就是区地图,通过研究它,全表扫描操作的流程我们应该也都清楚了。很简单,找到段头,读取区地图信息,根据区地图的顺序,读取每一个区。所以,全表扫描的显示顺序,就是区地图中区的顺序,其实也就是dba_extents中区的顺序。

下面再来看一下全表扫描的逻辑读。

SQL> drop tablespace tbs_ts2 INCLUDING CONTENTS;

Tablespace dropped.

 

SQL> create tablespace tbs_ts2 datafile '/u01/Disk1/tbs_ts2_01.dbf' size 20m reuse

uniform size 40k;

Tablespace created.

 

SQL> drop table table2;

Table dropped.

 

SQL> create table table2(id int,name varchar2(20)) tablespace tbs_ts2;

Table created.

 

SQL> insert into table2 values(1,'ABC');

1 row created.

 

SQL> commit;

Commit complete

SQL> set autot trace

SQL> select * from table2;

 

……………………

Statistics

----------------------------------------------------------

0  recursive calls

0  db block gets

4  consistent gets

0  physical reads

0  redo size

594  bytes sent via SQL*Net to client

520  bytes received via SQL*Net from client

2  SQL*Net roundtrips to/from client

0  sorts (memory)

0  sorts (disk)

1  rows processed

注意,做观察逻辑读的测试时,对测试SQL语句select * from table2,要反复多次执行。

这里重新创建了一个TBS_TS2表空间,它的区大小只有40KB,也就是5个块。然后建了一个表,随便插入一行,插入的这一行将会使高水点被抬升到区的最后一个块之后。

这个区只有5个块,前三个块分别是L1、L2和段头,可以存放用户数据的只有第4、5两个块,那么高水点将在第5个块之后。

为什么逻辑读是4次呢?全表扫描,要跳过L1、L2,只读段头和高水点下的所有块,也就是读段头和第4、5个块。但是段头要读两次,所以,逻辑读为4。至于段头读两次的原因,根据前面DUMP的段头来看,段头中的Extent Map、Auxillary Map信息是分开存放的,要一次读Extent Map,一次读Auxillary Map,所以就要读两次了。

如何确定段头读两次的问题呢?Oracle 10G以前的版本,可以观察Latch的Gets次数,但在11GR2后,就只有使用DTrace跟踪才能知道了。本书后面章节会有些这方面的内容,我们会逐步深入到Oracle内部,揭开Oracle之谜。

1.2.6 索引范围扫描的操作流程

索引范围扫描,网上已经有很多讨论了,就是按照根、枝、叶的顺序读取。叶块的地址在枝块,枝块地址在根块。找到枝块就可以找到叶块,找到根块就可以找到枝块。那么,如何找到根块呢?

其实很简单,在Oracle中,根块永远在索引段头的下一个块处。因此,索引扫描是不必读取索引段头的。先在数据字典表中找到段头位置,块号加1就是根块位置了。

对索引范围扫描时的逻辑读,可以做如下测试:

SQL> insert into table1 select rownum,'abcde' from dba_objects;

12691 rows created.

SQL> commit;

Commit complete.

SQL> create index table1_id on table1(id) tablespace tbs_ts1;

Index created.

 

SQL> exec dbms_stats.gather_table_stats('LHB','TABLE1');

PL/SQL procedure successfully completed.

 

SQL> select BLEVEL from dba_INDEXES where index_name='TABLE1_ID' and owner='LHB';

 

    BLEVEL

----------

         1

上面先向表中插入了10000多行,再创建了一个1层高的索引,索引只有Root块和叶块。

下面看看索引访问一次的逻辑读:

SQL> set autot trace

SQL> select * from table1 where id=10;

Statistics

----------------------------------------------------------

0  recursive calls

0  db block gets

4  consistent gets

0  physical reads

0  redo size

596  bytes sent via SQL*Net to client

520  bytes received via SQL*Net from client

2  SQL*Net roundtrips to/from client

0  sorts (memory)

0  sorts (disk)

1  rows processed

将测试SQL语句select * from table1 where id=10多执行几次,观察到的逻辑读为4。这4次逻辑读分别是:Root块一次,叶块两次,数据块一次。

叶块之所以需要两次,是因为索引是非唯一的。第一次读叶块是为了取出目标行ROWID,第二次读叶块是判断此叶块中还有没有满足条件的行。

如果建成了唯一索引,不需要判断叶块是否还有满足条件的行,叶块就只需要读一次,一共只需要3次逻辑读。

SQL> drop index table1_id ;

Index dropped.

 

SQL> create UNIQUE index table1_id on table1(id) tablespace tbs_ts1;

Index created.

 

SQL> set autot trace

SQL> select * from table1 where id=10;

Statistics

----------------------------------------------------------

0  recursive calls

0  db block gets

3  consistent gets

0  physical reads

0  redo size

460  bytes sent via SQL*Net to client

509  bytes received via SQL*Net from client

1  SQL*Net roundtrips to/from client

0  sorts (memory)

0  sorts (disk)

1  rows processed

表空间和存储结构这就说到这儿。本章中的例子,都是在Oracle 11GR2中做的,在Oracle 10g中做同样例子时的注意事项也都随例子说明了。

另外,本章的测试都是以8KB块大小为例的,其他块大小下的情况,留给读者亲自动手测试。

上一篇:让Windows XP SP3远程桌面连接支持网络级身份验证的脚本


下一篇:进入Gartner数据库魔力象限领导者的产品究竟有哪些过人之处?阿里云数据库产品手册开放下载!