数据库集簇的逻辑结构
数据库集簇(database cluster)是指由单个PostgreSQL服务器实例管理的数据库集合。(解读:数据库集簇是集合,其元素是数据库。一个PostgreSQL服务器实例只会在单机上运行并管理单个数据库集簇。注意这里表述中的两个”单“,即单机单个集簇,说明服务器实例不能跨多个主机,不能管理多个数据库集簇)。数据库集簇在本质上就是一个文件目录,其包含着一些列子目录与文件。(例如执行/opt/pgsql/bin/initdb -D /pgdata/10/data -W 在指定目录下创建基础目录,从而初始化一个新的数据库集簇,这里的/pgdata/10/data就是数据库集簇)基础目录文件树见文末。
注:PostgreSQL的数据库集簇(database cluster)概念和高可用数据库集群不同,这里的集簇(cluster)仅表示多个逻辑的数据库在用一个数据库实例中。
逻辑结构
区分数据库和数据库实例:PostgreSQL数据库是由一系列位于文件系统上的物理文件组成(通常这些物理文件被称为数据库);物理文件加上进程管理的内存和管理这些物理文件的进程称为该数据库的实例。在上述初始化数据目录的操作中,initdb工具自动创建了template0、template1和postgres数据库(解读:postgres是默认数据库,template0和template1是生成其他数据库的模板)。一个数据库集簇可以包含多个数据库、多个User,梅特数据库以及数据库中的表、索引等都有它们的拥有者:User。
创建一个Database时会为这个Database创建一个名为public的默认Schema,每个Database可以多个Schema,在数据库中创建表、索引等时如果没有指定Schema,都会在public这个Schema中。(解读:Schema可以理解为一个数据库中的命名空间,在数据库中创建的所有对象都在Schema中创建,一个用户可以从同一个客户端连接中访问不同的Schema)。不同的Schema中可以有多个相同名称的表、索引、视图等。
数据库对象
数据库是数据库对象(database object)的集合(例如,索引、视图、函数等都是数据库对象)。在PostgreSQL中,数据库本身也是数据库对象,并在逻辑上彼此分离。除数据库之外的其他数据库对象(如表、索引等)都归属于各自相应的数据库。虽然隶属于同一个数据库集簇,但无法直接从集簇中的一个数据库访问到。
表空间也是数据库对象,在PostgreSQL中最大的逻辑存储单元就是表空间。数据库中创建的对象,包括数据库本身都保存在表空间中(例如表、索引和整个数据库)。在创建数据库对象时,可以指定数据库对象的表空间,如果不指定则使用默认表空间。初始化数据库目录时会自动创建pg_default和pg_global两个表空间。
- pg_global表空间的物理文件位置在数据目录的global目录中,它用来保存系统表。
- pg_default表空间的物理文件位置在数据目录中的base目录,是template0和template1数据库的默认表空间。创建数据库时,默认从template1数据库进行克隆。因此除非特别指定新建数据库的表空间,默认使用template1的表空间。
在PostgreSQL内部,所有数据库对象都通过相应的对象标识符(object identifier, oid)进行管理,这些标识符是无符号的4字节整型。数据库对象和各个oid之间的关系存储在适当的系统目录中,具体取决于对象的类型。
- 数据库的oid存储在pg_database系统表中。
- 数据库中的表、索引、序列等对象的oid存储在pg_class系统表中。
从base文件目录下可以看到template0、template1和postgres对应的oid文件,也即相应的数据库文件。
pg_class系统表的oid为1259,它位于base目录postgres目录下,处于pg_default表空间中。
pg_database系统表的oid为1262,它位于global目录下,处于pg_global表空间中。
pg_default表空间的oid为1663,但是没有找到以1663命名的文件夹。
pg_global表空间的oid为1664,但是没有找到以1664命名的文件夹。
OID通常是从1开始分配,但在初始化数据集簇时,会先将一部分OID分配给系统表、系统表元祖、系统表上的索引等数据库对象,这一部分OID可以在系统表所对应的头文件中找到。同时,为了给后续版本留下扩展的余地,初始化数据集簇时还会预留一部分OID资源。这样,在系统运行时可分配的OID资源实际是从16384开始的。在PostgreSQL源代码src/include/catalog子目录下有一个shell脚本unused_oids用来输出当前版本中预分配和预留的OID的使用情况。
对于用户表的元祖,是可以在创建用户表时选择是否具有OID属性的。如果在CREATE TABLE语句中使用了WITH OIDS选项,则该用户表中插入的每一个元祖都将被分配一个OID。否则,默认状态下用户表的元祖是没有OID属性的。
OID的分配由系统中的一个全局OID计数器来实现,每次需要分配新的OID时,就从该计数器中取当前的OID,然后该计数器将会加1。OID分配时会采用互斥锁加以锁定以避免多个要求分配OID的请求获得同一个OID。
数据库的布局
如下图所示,一个数据库与base子目录下的一个子目录对应,且该子目录的名称与相应数据库的oid相同。例如,当数据库sampledb的oid为16384时,它对应的子目录名称就是16384。数据库中的每个表和索引都至少在相应子目录下存储为一个文件。
表空间的布局
PostgreSQL中的表空间对应一个包含基础目录之外数据的目录(附加数据区域)。执行CREATE TABLESPACE语句会在指定目录下创建表空间。在该目录下还会创建版本特定的子目录(PG_主版本号_目录版本号)。每个用户定义的表空间在PGDATA/pg_tblspc目录里面都有一个符号链接,它指向表空间的物理目录,该符号链接用表空间的OID命名。系统默认创建的表空间pg_default和pg_global并没有通过符号链接的方式指向其物理目录,而是直接对应PGDATA/base和PGDATA/global。
如果在mytblspc下创建新表,但新表所属的数据库却创建在基础目录下,那么PG会首先在版本特定的子目录下创建名称与现有数据库oid相同的新目录,然后将新表文件放置在刚创建的目录下。如下图Database Directory所指示的一样。
数据目录结构
/pgdata/10/data
|--- PG_VERSION PostgreSQL主版本号文件
|--- postgresql.conf 全局配置文件,除认证外其他行为由其配置
|--- postgresql.auto.conf 只保存ALTER SYSTEM命令修改的参数
|--- pg_hba.conf 全局配置文件,负责客户端的连接和认证配置
|--- pg_ident.conf 控制PostgreSQL用户名映射
|--- base
|--- 1 template1数据库目录
|--- 13213 template0数据库目录
|--- 13214 postgres数据库目录
|--- pg_serial 包含已提交的可序列化事务信息的子目录,初始化后为空目录
|--- pg_tblspc 包含指向表空间的符号链接的子目录,初始化后为空目录
|--- global 包含集簇范围的表的子目录,比如pg_database
|--- pg_logical 包含用于逻辑复制的状态数据的子目录
|--- pg_snapshots 包含导出的快照的子目录
|--- pg_twophase 用于预备事务状态文件的子目录
|--- pg_commit_ts 事务提交的时间戳数据
|--- pg_multixact 多事务状态数据
|--- pg_stat 统计子系统的永久文件
|--- pg_dynshmem 动态共享内存子系统中使用的文件
|--- pg_notify LISTEN/NOTIFY状态数据
|--- pg_stat_tmp 统计子系统的临时文件
|--- pg_wal WAL段文件,从pg_xlog重命名而来
|--- pg_replslot 复制槽数据
|--- pg_subtrans 子事务状态数据
|--- pg_xact 事务提交状态数据,从pg_clog重命名而来
数据库集簇的物理结构
每个表和索引都存储在其所属数据库目录下的独立文件里,以该表或者该索引的filenode号命名,该号码记录在该表或索引在系统表pg_class中对应元祖的relfilenode属性中。在表或索引超过1GB之后,它就被分裂成多个1GB大小的段。第一个段的文件名和filenode相同,随后的段命名为filemode.1,filenode.2 ......这样的策略避免了在某些有文件大小限制的平台上可能出现的问题。如果一个表的有些属性要存储相当大的数据,那么就会有个与之相关联的TOAST表,用于存储无法在数据行中放置的超大外置数据。表对应的pg_class元祖的reltoastrelid属性记录了她的TOAST表OID。
表和元组的组织方式:在PG中,同一个表中的元组按照创建顺序依次插入到表文件中,元组之间不进行关联,这样的表文件称为堆文件。PG系统中包含了四种堆文件:普通堆(ordinary cataloged heap)、临时堆(temporary heap)、序列(SEQUENCE relation,一种特殊的单行表)和TOAST表(TOAST table)。临时堆的结构与普通堆相同,但临时堆仅在会话过程中临时创建,会话结束会自动删除。序列则是一种元组值自动增加的特殊堆。TOAST表其实也是一种普通堆,但是它被专门用于存储变长数据。尽管这几种堆文件功能各异,但在底层的文件结构却是相似的。每个堆文件都是由多个文件块组成。数据的读写是以块为单位,块默认大小为8K,在编译PG时指定的BLCKSZ决定块的大小。
文件块在物理磁盘上的存储形式如下。Page Header Data是长度为20字节的页头数据,包含该文件块的一般信息,如空闲空间的起始和结束位置、Special space的开始位置、项指针的开始位置、标志信息,如是否存在空闲项指针、是否所有的元组都可见。
Linp是ItemIdData类型的数组,ItemIdData类型由lp_off、lp_flags和lp_len三个属性组成。每个ItemIdDate结构用来指向文件块中的一个元组,其中lp_off是元组在文件块中的偏移量,而lp_len则说明了该元组的长度,lp_flags表示元组的状态(分为未使用、正常使用、HOT重定向和死亡四种状态)。每个Linp数组元素的长度为4字节。
Freespace是指未分配的空间(空闲空间),新插入页面中的元组及其对应的Linp元素都将从这部分空间中来分配,其中Linp元素从Freespace的开头开始分配,而新元组数据则从尾部开始分配。
Special space是特殊空间。用于存放与索引方法相关的特定数据,不同的索引方法在Special space中存放不同的数据。由于索引文件的文件块结构和普通表文件的相同,因此Special space在普通表文件块中并没有使用,其内容被置为空。
参考:
PostgreSQL实战
博客中的流程图均使用ProcessOn绘制(一款在线流程图绘制软件),网址:https://www.processon.com/
转载请注明出处