1.1 区:表空间中的基本单位
区,Extent,逻辑上连续的空间。它是表空间中空间分配的基本单位。如果在某表空间中创建一个表,哪怕只插入一行,这个表至少也会占一个区。
具体来讲,在Oracle 10g中,如果创建一个新表,初始至少为这个表分配一个区。而在Oracle 11.2.0.3以上版本中,创建新表时默认一个区都不会分配,也就是说,这个表此时不占存储空间。只有在向表中插入第一行数据时,才会默认为表分配第一个区。
无论是Oracle 10g还是Oracle 11GR2,如果表原有区中的空间用完了,Oracle就会默认为表一次分配一个区的空间。
可以通过DBA_EXTENTS数据字典视图查看表所属区。假设有一个表Table1,想要查询它所在的区,可通过如下方式:
select extent_id, file_id, block_id, blocks from dba_extents where
segment_name='TABLE1' order by extent_id;
上面语句中,每个列的意义都很简单,这里不再介绍,如有问题,可以查看Oracle联机文档Oracle Database Reference中的视图介绍。
说了这么多,大家会不会有一个疑问:既然区这么重要,是空间分配的基本单位,那么,区的大小是如何定义的呢?
Oracle专门设定了两种类型的表空间:统一区大小表空间和系统管理区大小表空间。区的大小就是由这两种表空间决定的。下面,先从统一区大小讲起。
1.1.1 统一区大小表空间和区的使用规则
统一区大小的表空间理解起来很简单,顾名思义,就是创建表空间时,设定区大小为一个统一的值。如下命令创建一个区大小为1MB的表空间:
create tablespace tbs_ts1 datafile '/u01/Disk1/tbs_ts1_01.dbf' size 50m uniform size 1m;
tbs_ts1表空间包含一个50MB的数据文件,区大小为1MB。在此表空间中创建一个测试表:table1,观察一下区大小。
SQL> create table table1(id int,name varchar2(20)) tablespace tbs_ts1;
Table created.
SQL> insert into table1values(1,'VAGE');
1 row created.
SQL> commit;
Commit complete.
SQL> select extent_id, file_id, block_id, blocks from dba_extents where
segment_name='TABLE1' order by extent_id;
EXTENT_ID FILE_ID BLOCK_ID BLOCKS
--------------------- --------------------------
0 4 128 128
可以看到,table1表目前只包含一个区,它从4号文件的第128号块开始,大小为128个块。笔者这里的块大小为8KB,128个块,正好就是1MB。
从上面的结果可以看到,表table1从4号文件的128号块开始占用空间。从128~257号块是table1的第一个区,那么,0~127号块又是干什么用的呢?
事实上,每个文件的前128个块,都是文件头,被Oracle留用了。在Oracle 10g中是0至8号块被Oracle留用。而从Oracle 11GR2开始,一下就留用128个块,真是大手笔,不是吗?
这一部分文件头又分两部分,其中0号、1号块是真正的文件头,2~127号块是位图块。而在Oracle10g中,2~8号块则是位图块。
这个位图块又是干什么用的呢?
很容易理解,是用来记录表空间中区的分配情况的。位图块中的每一个二进制位对应一个区是否被分配给某个表、索引等对象。如果第一个二进制位为0说明表空间中第一个区未分配,如果为1说明已分配;第二个二进制位对应第二个区,以此类推,如图1-1所示。
图1-1 位图块示意图
在图1-1中,上面的二进制位就是位图块中的数据,下面表格表示区,其中灰色区是已分配区,白色区是未分配区。第1个区对应的二进制位是1,代表此区已分配,第2个区对应的二进制位是0,表示此区未分配,等等。
位图块又分两部分,其中第一个位图块又被当作位图段头,可以在DUMP文件中找到Oracle对此块类型的说明:Bitmapped File Space Header。从第二个位图块也就是3号块开始,就是真正的位图数据了,DUMP文件中这些块的类型说明为:Bitmapped File Space Bitmap。
关于位图块的DUMP格式描述,大家可以在网上找一下,此处不赘述。下面讨论一个网上较少讨论的话题:当要分配区时,Oracle如何在位图块中搜索可用区。
大家可以考虑一个问题,如果块大小为8KB,0号、1号块是文件头,2号块是位图头,在Oracle 10g中,3~8号块是位图数据块,共6个位图块,大小是48K字节,每个字节8个二进制位,一共393216个二进制位。每个二进制位对应一个区,一共就393216个区。如果是Oracle 11GR2,这个数字又要多出好多倍。那么,如果某个表要在数据文件中分配一个新区,Oracle如何在这30多万个二进制位中,确定哪个二进制位对应的区可以分配给表呢?
Oracle用的方法其实很简单,在位图块中,找一个标记位,如果0~2号区被占用了,标记位的值为3;如果3~4号区又被占用了,标记位增加为5。假设此时2号区被释放了,标记位变为2。
如果需要分配新的区,从这个标记位处开始查找即可。假设目前标记位值为5,有进程需要4个未分配区,Oracle就从5号区开始向下查找。
需要注意的是,如果开启了闪回Drop,而且Drop了表,那么,区并不会被释放,因此标记位不会下降。因为Drop只是改名,而并不会真正删除表。
1.1.2 系统管理区大小
刚才介绍了统一区大小,并且测试了区的分配规则。在系统管理区中,区的分配规则是一样的,但是,区大小的设定不再由人为决定,Oracle会根据表大小自动设置。
Oracle如何设定区大小呢?只需要创建一个表空间,并进行很简单的测试,就能搞明白这点。命令如下所示:
create tablespace tbs_ts2 datafile '/u01/Disk1/tbs_ts2_01.dbf' size 50m reuse;
上面的命令创建了一个tbs_ts2表空间,至于区大小的管理方式,这里没有指定,这样Oracle默认创建的,就会是系统管理区的大小。
现在在tbs_ts2中创建表,并观察它的区大小。
SQL> create table table_lhb1(id int,name varchar2(20)) tablespace tbs_ts2;
Table created.
SQL> select extent_id, file_id, block_id, blocks from dba_extents where segment_
name='TABLE_LHB1' order by extent_id;
EXTENT_ID FILE_ID BLOCK_ID BLOCKS
---------- ---------- ---------- ----------
0 5 128 8
可以看到TABLE_LHB1目前有一个区,大小只有8个块,也就是64KB。插入一些数据,将表撑大点再观察。
SQL> insert into table_lhb1 select rownum,'aaa' from dba_objects;
12650 rows created.
SQL> select extent_id, file_id, block_id, blocks from dba_extents where segment_
name='TABLE_LHB1' order by extent_id;
EXTENT_ID FILE_ID BLOCK_ID BLOCKS
---------- ---------- ---------- ----------
0 5 128 8
1 5 136 8
2 5 144 8
3 5 152 8
上面向表中插入了12650行,表目前涉及4个区,每个区都是8个块64KB。提交后继续插入,命令如下:
SQL> insert into table_lhb1 select * from table_lhb1;
12650 rows created.
SQL> insert into table_lhb1 select * from table_lhb1;
25300 rows created.
SQL> insert into table_lhb1 select * from table_lhb1;
50600 rows created.
SQL> insert into table_lhb1 select * from table_lhb1;
101200 rows created.
SQL> commit;
Commit complete.
SQL> select extent_id, file_id, block_id, blocks from dba_extents where segment_
name='TABLE_LHB1' order by extent_id;
EXTENT_ID FILE_ID BLOCK_ID BLOCKS
---------- ---------- ---------- ----------
0 5 128 8
1 5 136 8
2 5 144 8
3 5 152 8
4 5 160 8
5 5 168 8
6 5 176 8
7 5 184 8
8 5 192 8
9 5 200 8
10 5 208 8
11 5 216 8
12 5 224 8
13 5 232 8
14 5 240 8
15 5 248 8
16 5 256 128
17 5 384 128
18 rows selected.
多次插入后,表已经占了很多空间,我们来看一下现在区的使用情况。0~15号区,大小为8个块64KB,从第16号区开始,区大小变为了128个块1MB大小。也就是说,表的大小小于1MB时,表的每个区都是64KB,当表的大小超过1MB,再分配新区时,区的大小将是1MB。读者可以继续测试,当表进一步变大,区大小将会变成8MB,表继续扩大,更大的区也有,这里就不测了。
可见,在系统管理区大小表空间中,区的大小随表的增大而增大。
到这里,我们已经发现了系统管理区大小的秘密。很简单吧?有时候探索Oracle也不是件复杂的事情,只要多动手就行了。我在做培训的时候,也常跟学员讲:探索的过程,就是熟悉Oracle的过程。你探索出的结果不一定很有用,但探索的过程,会加深对Oracle的熟悉程度,这才是研究、探索Oracle的真正目的。研究Oracle,很多时候结果不是目的,过程更有意义。
我们已经了解了统一区大小和系统管理区大小的不同,那么,什么时候使用统一区大小,什么时候使用系统管理区大小呢?
从空间的利用率上讲,小区节省空间,大区可能会浪费空间。比如,当区大小是10MB时,为一个表分配了一个10MB的区,哪怕它只使用了这10MB中的1个字节,这10MB空间也完全属于这个表了,其他表无法再使用这部分空间。从这个角度上讲,小区的空间利用率无疑是高的。
但从性能角度上讲,对于随机访问,大区、小区没有影响。但对于全表扫描这样的操作,大区又是更合适的。因为连续空间更多,可以减少磁头在区间的定位。
在系统管理区大小的方式下,当表比较小时,区也比较小,当表大时,区也随之变大,这种方式无疑可以在空间的利用率、全扫描的性能之间找到一种平衡。因此建议大多数情况下,都可以采用系统管理区大小的方式。除非有某个表,已明确地知道它会很大,为了保证全扫描的性能,直接建一个统一区大小,并且区比较大的表空间,以便将表存放其中。
如果使用统一区大小,几百KB甚至1MB的区,都有点小。其实可以参考系统管理型的表空间,当段的大小超过64MB时,区大小为8MB。在使用统一区大小时,也可以将所有区都固定为8MB。
我见到过很多数据库都使用统一区大小,而且其大小为1MB。原因是在大部分的操作系统中,一次I/O操作的最大的读、写数据量是1MB。即使使用8MB的区,一个区也必须分8次进行I/O操作,超过1MB的区大小,并不能减少I/O操作的次数。
但是,我们要考虑一点,8MB的区连续的空间更多。读取8MB内的第1MB和第2MB数据虽然必须要分两次I/O操作,但这两次I/O操作很可能是连续I/O,因为第1MB和第2MB数据有可能是相连的。如果区大小仅为1MB,虽然读取表的第1区和第2区也是两次I/O操作,但这两次I/O操作很可能不相连,是随机I/O操作。连续I/O操作的性能当然比随机I/O操作的要高。
因此,出于全表扫描性能的考虑,即使使用统一区大小,大点的区(如8MB大小)是很合适的选择。
还有一个问题,不知道大家有没考虑到。这个问题涉及统一区大小表空间的位图块。每个二进制位对应一个区的使用情况,这是没问题的,但系统管理区大小呢?就比如刚才创建的TABLE_LHB1表,前16个区大小为64KB,之后的区大小为1MB。区的大小不同,如何用二进制位来反映区的使用情况呢?
Oracle的处理方法是这样的,以64KB(也就是8个块)为准,每个二进制位对应64KB。1MB的区,对应16个二进制位。每分配一个1MB的区,Oracle将对应的16个二进制位(也就是两个字节)设置为1。释放一个区也同样,将16个二进制位设置为0。这样就解决了区大小不统一的问题,Oracle的解决方法还是很巧妙的!
1.1.3 碎片:少到可以忽略的问题
最后,来想这样一个问题:在表空间级别有碎片吗?
答案是:有,但也要看情况。在统一区大小表空间中,因为区的大小一致,不会出现碎片问题。但在系统管理区中,由于区的大小不一致,仍会存在碎片。比如说,有很多个64KB的区,互相不连续,分布在数据文件的各个角落。当需要1MB、8MB大小的区时,这些不连续的64KB区无法被重用,这就是典型的碎片了。但是,这种情况很少出现。因为区不会被频繁地分配、释放。一个表创建之后,很少会去对它进行Drop、Truncate操作。
没有频繁的分配、释放操作,碎片也就很少出现了。所以,在表空间层,碎片已经是一个可以忽略的问题了。当然,在表层、索引层,还有可能存在碎片。