本文所讨论的内容涉及的数据库版本为 Oracle 19c。
Oracle数据库为数据库中的所有数据分配逻辑空间。数据库分配空间的逻辑单位是数据块(data blocks)、区(extents)、段(segments)和表空间(tablespaces)。从物理层面来说,数据都存储在磁盘上的数据文件中。而数据文件中的数据存储在操作系统块中。
一个段包含一个或多个区,而一个区包含多个数据块。下图展示了一个表空间内部数据块、区、段之间的等级关系。
从最低粒度到最高粒度,Oracle数据库存储数据的逻辑结构如下:
Oracle数据必须使用逻辑空间管理来跟踪和分配表空间中的区。当有数据库对象请求一个区时,数据库必须有办法找到并提供该区;当一个区不再被需要时,数据库也必须有办法释放该区使其可用。
Oracle数据库使用下面的办法来管理表空间内的空间:
#1 本地管理的表空间
本地管理(locally managed)的表空间中,在数据文件头(header)会维护一个位图,用以跟踪数据文件中空闲的和已使用的空间。每个比特都对应一组数据块。当有空间被分配或者释放时,Oracle数据块会修改位图的值来更新数据块的状态。其中,1表示空间已被使用,0表示空闲。
使用本地管理的表空间有以下几点好处:
在本地管理的表空间中,段空间可以自动或者手动管理。段空间自动管理(automatic segment space management, ASSM)也是使用位图来实现的。段空间手动管理(manual segment space management, MSSM)则使用一个被称为 free list 的链表结构来管理空闲的段。
#2 字典管理的表空间
该方法使用数据字典(data dictionary)来管理区。当有区被分配或者被释放时,Oracle数据库会更新数据字典中的表。
Oracle数据库中的数据块又被称为Oracle块(Oracle block)或者页(page)。数据块是数据块 I/O 的最小单位。
在物理层面,数据库的数据都存储在由操作系统块(operating system blocks)组成的磁盘文件中。操作系统块是操作系统可以读写的最小数据单元。而Oracle块则是一个逻辑存储结构,它的大小和结构对于操作系统而言是未知的。
当数据库请求一个数据块时,操作系统将该操作翻译为请求永久存储中的数据。数据块与操作系统块在逻辑上的分割有如下影响:
#1 数据库块大小
每个数据库都有一个数据库块大小。在创建数据库时,初始化参数 DB_BLOCK_SIZE
设定了数据库的数据块大小。这个参数值对 SYSTEM
和 SYSAUX
表空间生效,同时也是其他所有表空间的默认值。只有重新创建数据块,才能改变该参数的大小。
如果 DB_BLOCK_SIZE
的值未设定,那么其值取决于操作系统。一般是 4KB 或者 8KB,且必须是操作系统块的倍数(或者相等)。
#2 表空间块大小
你也可以创建数据块大小不同于 DB_BLOCK_SIZE
的单独的表空间。非标准大小的表空间可以用于将表空间迁移到其他平台。
每个数据块都有能够使数据库跟踪其中数据及其可用空间的内部结构或格式。且无论数据块是否包含表、索引或集群数据时,其格式都是相似的。下面展示了未压缩数据块的格式。
#1 数据块头部
Oracle数据块使用块头部(Block overhead)来管理数据块本身。块头部中有些部分的大小是固定的,但是块头部的总大小是可变的。平均来说,块头部一般总共占用84字节到107字节的空间。块头部不用于存储用户数据。块头部包含以下部分:
数据块头(Block header):存有数据块的基本信息,包括磁盘地址、段的类型。对于事务管理(transaction-managed)的数据块,header部分还存有活跃和历史事务信息。
每个更新数据块的事务都需要一条事务记录(transaction entry)。Oracle数据库在数据块头中为事务记录预留了空间。在header部分空间耗尽时,空闲空间(Free space)也可以用来存储事务记录。大多数操作系统中,数据块中事务记录所需的空间大致为23 bytes。
表目录(Table directory):
对一个堆组织(heap-organized)的表而言,表目录包含了关于在该数据块中存有行数据的表的元数据信息。在表集群(table cluster)中,多个表可以将行数据存在同一个数据块中。
行目录(Row directory):
对一个堆组织的表而言,行目录描述了行数据在数据块中的位置。rowid指向一个特定的文件、数据块以及行编号(row number)。其中,行编号是行目录中一条记录(entry)的索引。行目录的记录中包含了一个指向数据块中行数据位置的指针(pointer)。如果数据库移除了数据块中的一条行数据,就会更新行目录中的记录来修改对应的指针。而rowid一直是保持不变。
数据库在行目录中分配了空间后,并不会回收已删除行对应的行目录空间。仅在有会话在数据块中插入新的行时,数据库才会重用该行目录空间。
#2 行数据格式
数据块的行数据(Row data)部分包含了真正的数据,例如表的行或者索引键记录。行数据也有自己的内部格式。Oracle数据将行数据存为长度可变的记录。一条行数据被存储在一个或多个 row piece 中。每个 row piece 都有一个行头(row header)和列数据(column data)。
Row header
Oracle数据库利用row header来管理数据块中的row piece。行头中包含以下部分:
一条完全存储在一个数据块中的行至少包含3个字节大小的row header。
Column data
列数据中存储了行中的真正的每一列的数据部分。Row piece通常按照 CREATE TABLE
语句中的顺序来存储列数据(但是并不保证完全按这个顺序)。比如,long类型的列数据通常最后被创建。
正如图5所示,对于row piece中的每个列,Oracle数据库会分开存储列的长度(column length)和列数据本身。每条行数据在数据块header的行目录中都对应有一个插槽(slot)。这个插槽会指向行数据的开头。
Rowid 格式
Oracle数据块使用 rowid 来唯一地标识一行。Rowid是一个包含了数据库可以用来访问一行数据的信息的结构。Rowid并不是以物理形式存储在磁盘上的,而是通过存储数据的文件和数据块推断出来的。
扩展的rowid包含了数据对象编号(data object number)。这种类型的rowid使用64位编码每行的物理地址,可用的编码字符包括 A-Z、a-z、0-9、+和/。以 OOOOOOFFFBBBBBBRRR
为例,可以分为以下四部分:
OOOOOO
:数据对象编号用于标识段(segment)。每个数据库段都会被分配一个数据对象编号。在同一个段中的schema对象都会有相同的数据对象编号。FFF
:表空间相关的数据文件编号用于标识存有行数据的数据文件。BBBBBB
:数据块编号用于标识存有行数据的数据块。数据块编号与数据文件有关,而不是表空间。因此,两条有相同数据块编号的行数据可以存储在同一个表空间的不同数据文件中。RRR
:行编号用于标识数据块中的行。Rowid在被分配给一个row piece之后,在特殊情形下可以被修改。
数据库可以使用表压缩(table compression)来消除数据块中的重复值。使用了压缩技术的数据块的内部结构与未压缩的数据块大致相同,区别在于:在压缩的数据块中会有一个符号表(symbol table),用于存储重复的行和列的数据值。数据库会把出现这些重复值的地方用符号表中的对应符号替换。
当数据库从数据块底部开始填充数据时,block header和row data之间的可用空间就会越来越少。数据库可以通过管理数据块中的可用空间来提升性能、避免空间浪费。
#1 数据块中的可用空间
SQL参数 PCTFREE
设定了在数据块中为更新现有行数据而保留的可用空间的最小百分比。PCTFREE
对于防止行迁移和避免空间浪费来说非常重要。
假设,你通过 CREATE TABLE
语句设定了 PCTFREE
的值:
CREATE TABLE test_table (n NUMBER) PCTFREE 20;
随着数据库向数据块中添加行,底部的行数据和顶部的块头部向中间靠拢,导致可用空间逐渐减少。而PCTFREE参数会保证至少有20%的数据块空间可用(free)。比如,如果有一个INSERT操作会导致可用空间减少到10%,那么数据库会阻止该插入操作。
#2 可用空间的优化
一些DML操作可以增加数据块中可用空间的大小。另一方面,被释放的空间在数据块中与可用空间的主要区域也许并不是相邻的,通过合并这些碎片化的空间也可以优化可用空间。
以下的SQL操作可以增大可用空间:
仅仅当满足以下条件时,Oracle数据库会自动地、透明地合并(coalesce)数据块中的可用空间:
#3 行链接&行迁移
Oracle数据库使用行链接和行迁移来管理太大而无法放到单个数据块中的行。以下情形有可能发生:
LONG
或者 LONG RAW
数据类型的列,或者当行内包含了非常多的列,这类情况下行链接就是无法避免的。索引块(index block)是一种特殊的数据块,与表数据块(table blocks)管理空间的方式不同。
#1 索引块分类
一个索引包括一个根块(root block)、枝块(branch blocks)和叶块(leaf blocks)。
#2 索引记录存储
索引记录(index entries)存储在索引块中的方式与表行存储在数据块中的方式相同。
数据库管理索引块中行目录的方式不同于数据块中的行目录。行目录中的记录按键值排序。比如,索引键 000000
在行目录中的记录排在索引键 111111
对应的记录之前。这么做可以提高索引扫描的效率。
相对于一个堆组织的表数据块而言,一个索引块中通常包含更多的行。这使得数据库更加容易管理索引,因为避免了为了容纳新数据而可能导致的索引块的频繁分裂。
References
[1] https://docs.oracle.com/en/database/oracle/oracle-database/19/cncpt/logical-storage-structures.html#GUID-13CE5EDA-8C66-4CA0-87B5-4069215A368D