Oracle内核技术揭秘 -- 存储结构

发布时间 2023-12-15 17:32:02作者: guapisama

区:表空间中的基本单位

在Oracle 11.2.0.3以上的版本中,创建新表默认不会分配区给这个表的,只有在插入了数据之后才会分配一个区给这个表空间。区是表空间中空间分配的基本单位,如果一个区的空间用完了,Oracle就会默认再分配一个区。

Oracle专门设定了两种类型的表空间:统一大小表空间和系统管理区大小表空间。区的大小就是由这两种表空间决定的。

表空间大小限制

表空间数据文件容量与DB_BLOCK_SIZE有关,ORACLE的物理文件最大只允许4194304个数据块,表空间数据文件的最大值为 4194304×DB_BLOCK_SIZE/1024M。

4k最大表空间为: 16384M

8K最大表空间为: 32768M

16k最大表空间为:65536M

32K最大表空间为:131072M

64k最大表空间为:262144M

统一大小表空间和区的使用规则

统一区大小的表空间就是创建表空间时设定区大小为一个统一的值。

create tablespace tbs_ts1 datafile '/u01/disk/tsqg.dbf' size 50m uniform size 1m;

事实上,每个文件的前128个块,都是文件头,被Oracle留用了。在10G的时候只留用0-8号块,在11GR2开始直接留用128个块。

这一部分又分两个部分,其中0号、1号块是真正的文件头,2-127号块是位图块。在Oracle10g中,2-8号块是位图块。位图块是用来记录表空间中区的分配情况的。位图块中的每一个二进制位对应一个分区是否被分配给某个表、[[索引]]等对象。如果为0表示对应区未分配,1表示已分配。

位图块又分为两部分,其中第一个位图块又被当做位图段头,可以在DUMP文件中找到Oracle对此块类型的说明:Bitmapped File Space Header。从第二个位图块也就是3号块开始就是真正的位图数据了,DUMP文件中这些块的类型说明为:Bitmapped File Space Bitmap

那么如何确定哪个二进制位对应的区可以分配给表呢?

在位图块中找一个标记位,如果0~2号区被占了,标记位的值为3;如果3~4号区又被占了,标记位增加为5。此时2号区被释放了,标记位变为2。如果要分配新的区就会从标记位开始向下查找。此处注意,如果开启了闪回Drop,而且Drop了表,那么区不会被释放,因此标记位不会下降,因为Drop只是改名,而不会真正删除表。

系统管理区大小

系统管理区大小不能设置,Oracle会根据表的大小自动设置,在系统管理区大小表空间中,区的大小随表的增大而增大。

那么什么时候使用统一表空间大小,什么时候使用系统表空间大小?

从利用率上讲,小区节省空间,大区会造成空间的浪费,因为他每次分配都会直接分配一整个区;从性能角度上讲,随机访问对于大区小区没有影响,但是对于全表扫描这样的操作,大区这样连续空间更多无疑是更有优势的。

大多情况下使用系统管理区,除非你知道这个表很大,为了保证全表扫描的性能,使用大区。

还有一个问题是系统表空间大小是不固定的,那么怎么使用二进制位来反映区的使用情况?Oracle会统一以64KB为准,每个二进制位对应64KB。1MB的区对应16个2进制位,将其统一设置为0或者1。

碎片:少到可以忽略的问题

表空间级别是有碎片的。再统一区大小表空间中,因为区的大小一致,不会出现碎片问题。但是在系统管理区中,由于区的大小不一致,仍会存在碎片。例如有很多64kb的区在各个角落,但是这时候想申请1mb之类的大区,那么那些64kb的不连续的区就无法被使用。

段中块的作用

首先要知道什么是段。在Oracle中,表和段是两个截然不同的概念,表是逻辑上说明了表的形式,例如表的列、类型、长度,这些信息都属于表。但是段只代表储存空间,上文提到的区就属于段。一个段中至少包含一个区。

块中空间的使用

一个块的大小最常见的是8KB。块中的信息分两部分:管理信息和用户数据,其中管理信息包括块头的SCN,ITL槽等。

如果想要了解如果删除一行,再回滚,行的位置会不会改变,可以使用包“dbms_rowid”,其作用是从rowid中将对象ID、文件号、块号、行号分解出来。或者把对象ID等合并成rowid。结果得出回滚后行的位置不会改变。

讲道理,在提交之前,删除某行就是给这行打上删除的标记表示可以覆盖,但是没有提交,事务在行上加的锁就不会释放,就不会被覆盖。而删除行的回滚就是将被删除的数据重新插入一次,但与直接插入不同的是,被删除行不会覆盖,所以回滚时的插入不会进行寻找空间的操作,而是行原来在哪就直接插入。

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

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

Oracle在插入行时与区的分配过程是类似的。Oracle会在数据块中设立一个标记位,记录空间使用到哪了。

在8192字节处插入5行数据,每行100字节,也就是说,空间已经使用到了(8192-500)7692字节处,那么标记位置为7692,但是删除一行并提交并不会导致标记位更改,标记位还是会顺着向下移动。当标记位的值越来越小,向上到达管理边界时,标记位会再次变为8192。

ASSM与L3、L2、L1块的意义

ASSM的目的是大并发插入,在输入输出能力满足的情况下,使用ASSM不一定能大并发插入。

首先先了解ASSM的整体结构:3层位图块+数据块的树状结构

 

L3块中可以存放多个L2块的地址,一个L2块中可以存放多个L1块的地址,一个L1块中可以存放多个数据块的地址。如果段头中存放了太多的L2块的信息,空间不足,Oracle会再分配第二个L3块。

Oracle如何利用4层树状结构来定位向哪个块中插入数据:

第一步,查找数据字典(dba_segments),确定段头位置

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

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

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

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

L3块中虽然有很多L2块,但是只会等第一个插满了才会选择下一个L2块。

简单总结,高水点移动,在ASSM下是以L1中数据块的数量为准。如果块大小是8KB,区大小是1MB,L1中有64个数据块,高水点就是以64个块为单位依次往后挪。也就是说,并发插入每次都只是向64个块中插入。所以如果有100个进程插入,但是只有64个块接收,将会有36个进程不得不和另一个进程同时向一个块中插入,这时会产生[[等待事件]]Buffer Busy Waits。

Oracle表段中的高水位线HWM
在Oracle数据的存储中,可以把存储空间想象为一个水库,数据想象为水库中的水。水库中的水的位置有一条线叫做水位线,在Oracle中,这条线被称为高水位线(High-warter mark, HWM)。在数据库表刚建立的时候,由于没有任何数据,所以这个时候水位线是空的,也就是说HWM为最低值。当插入了数据以后,高水位线就会上涨,但是这里也有一个特性,就是如果你采用delete语句删除数据的话,数据虽然被删除了,但是高水位线却没有降低,还是你刚才删除数据以前那么高的水位。也就是说,这条高水位线在日常的增删操作中只会上涨,不会下跌。HWM通常增长的幅度为一次5个数据块.

Select语句会对表中的数据进行一次扫描,但是究竟扫描多少数据存储块呢,这个并不是说数据库中有多少数据,Oracle就扫描这么大的数据块,而是Oracle会扫描高水位线以下的数据块。现在来想象一下,如果刚才是一张刚刚建立的空表,你进行了一次Select操作,那么由于高水位线HWM在最低的0位置上,所以没有数据块需要被扫描,扫描时间会极短。而如果这个时候你首先插入了一千万条数据,然后再用delete语句删除这一千万条数据。由于插入了一千万条数据,所以这个时候的高水位线就在一千万条数据这里。后来删除这一千万条数据的时候,由于delete语句不影响高水位线,所以高水位线依然在一千万条数据这里。这个时候再一次用select语句进行扫描,虽然这个时候表中没有数据,但是由于扫描是按照高水位线来的,所以需要把一千万条数据的存储空间都要扫描一次,也就是说这次扫描所需要的时间和扫描一千万条数据所需要的时间是一样多的。所以有时候有人总是经常说,怎么我的表中没有几条数据,但是还是这么慢呢,这个时候其实奥秘就是这里的高水位线了。

那有没有办法让高水位线下降呢,其实有一种比较简单的方法,那就是采用TRUNCATE语句进行删除数据。采用TRUNCATE语句删除一个表的数据的时候,类似于重新建立了表,不仅把数据都删除了,还把HWM给清空恢复为0。所以如果需要把表清空,在有可能利用TRUNCATE语句来删除数据的时候就利用TRUNCATE语句来删除表,特别是那种数据量有可能很大的临时存储表。

在手动段空间管理(Manual Segment Space Management)中,段中只有一个HWM,但是在Oracle 9i Release1才添加的自动段空间管理(Automatic Segment Space Management)中,又有了一个低HWM的概念出来。为什么有了HWM还又有一个低HWM呢,这个是因为自动段空间管理的特性造成的。在手段段空间管理中,当数据插入以后,如果是插入到新的数据块中,数据块就会被自动格式化等待数据访问。而在自动段空间管理中,数据插入到新的数据块以后,数据块并没有被格式化,而是在第一次访问这个数据块的时候才格式化这个块。所以我们又需要一条水位线,用来标示已经被格式化的块。这条水位线就叫做低HWM。一般来说,低HWM肯定是低于等于HWM的。

段头与Extent Map

上文说到,段头是第一个L3块,就是说段头中包含了L3信息。其实除了L3外还有Extent Map(区地图)。区地图顾名思义,记录了一个段中所有的区都在哪,全表扫描就是按地图区逐个读取所有的区。

Oracle会读dba_segments底层seg$这样的数据字典表。会先到共享池中的数据字典缓存中查找seg$相关的行,如果没有再到[[Buffer Cache]]中读seg$相关的块,还没有就从SESTEM表空间中读sge$表。

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

 

其中记录了第一个区开始自0x01000080处,这个区的大小是128个块。继续往下还有:

 

Auxillary Map——辅助地图,这部分更详细。L1 dba:0x01000080,说明了此区内**第一个L1块**开始的地方即4号文件的128号块。Data dba:0x01000084,说明用户数据开始的地方,即132号块。Oracle全表扫描时,是按照“Data dba:*****”后的DBA查找区的。但是这里没有区长度,所以上面那部分也要读。当大小超过64MB时,一个L1将存放256个数据块。

全表扫描逻辑读中为什么段头要读两次:段头中Extent Map、Auxillary Map信息是分开存放的,一次读Extent Map,一次读 Auxillary Map。

索引范围扫描的操作流程

索引范围扫描按照:根、枝、叶的顺序读取。找到根块就找到枝块,找到枝块就找到叶块。也就是说得先找到根块。根块永远在索引段头的下一个块处,也就是先在数据字典表中找到段头位置,块号加1就是根块的位置。PS:索引扫描不必读取索引段头的。

在测试环境中创建索引进行逻辑读发现读了4次:Root块一次,叶块两次,数据块一次。

之所以读了两次叶块是因为索引是非唯一的。第一次读叶块为了取出目标行ROWID,第二次判断次叶块中还有没有满足条件的行。如果是唯一索引,叶块就只需读一次,一共只需3次逻辑读。

 

END

摘自《Oracle内核技术解密》 ---吕海波