PostgreSQL 数据库集簇和表

1 背景知识

本文介绍 PostgreSQL 数据库集簇、数据库、数据对象。

2 数据库集簇的逻辑结构

2.1 数据库集簇

数据库集簇(database cluster)从逻辑方面来说是一组数据库(database) 的集合,由一套 PostgreSQL 服务节点进行管理。

Warning

请注意有些同学对这个定义也许会疑惑,这里要强调一点:
PostgreSQL 中的术语 数据库集簇并非 是指很多数据库服务器节点。

2.2 数据库(database)

  1. 数据库(database)数据库对象(database objects) 集合。
  2. 关系型数据库理论中,数据库对象是用于存储查询数据的数据结构
  3. (堆)表是一个典型的例子,还有更多种对象,例如索引,序列,视图,函数等。
  4. PostgreSQL数据库(database)本身也是数据库对象,并在逻辑架构上彼此分离。
  5. 所有其他的数据库对象(例如表,索引等)归属于各自对应的数据库。
     
     下图展示了一个数据库集簇的逻辑结构。
database1
database1
database2
database2
databaseN
databaseN
databases
databases
database cluster
database cluster
...
...
database
database
table1
table1
table2
table2
tableN
tableN
...
...
index1
index1
index2
index2
indexM
indexM
...
...
tables
tables
indexes
indexes
view1
view1
function1
function1
sequence1
sequence1
other objects
other objects
...
...
...
...
...
...
...
...
sch1
sch1
sch1
sch1
sch2
sch2
sch3
sch3
sch4
sch4
client
client
tablespace
tablespace
tablespace
tablespace
users
users
Text is not SVG - cannot display

2.3 模式(schema)

PostgreSQL 数据库提供一个默认名为 public 模式。也可以创建自己的模式。用于在同一个数据库中的区分不同的数据库对象。

2.4 数据库对象

  1. 关系型数据库理论中,数据库对象是用于存储查询数据的数据结构
  2. 数据库对象包括数据库、索引、序列、表、存储过程、函数等结构。
  3. (堆)表是一个典型的例子,堆表在逻辑上时 PostgreSQL 数据库存储数据和读取数据的存储结构。
  4. 数据库对象(例如表,索引等)归属于各自对应的数据库。
  5. 数据库(database) 也被称之为一个对象,叫做数据库对象。

2.5 数据库对象标识符(Object Identifiers, OID)

PostgreSQL 内部,所有的数据库对象都通过相应的对象标识符(Object Identifiers, OID) 进行管理,对象标识符(Object Identifiers, OID) 是无符号的 4 字节(bytes)整数类型。数据库对象与相应 对象标识符(Object Identifiers, OID) 之间的对应关系存储在相应的数据字典 中。

例如数据库堆表对象对象标识符(Object Identifiers, OID) 分别存储在 pg_databasepg_class 中。

2.5.1 对象标识符(Object Identifiers, OID) 和数据库对象的对应关系

因此当你想要找到某个对象的OID时,可以执行以下查询:

SELECT datname, oid FROM pg_database WHERE datname = 'testdb';
datname |  oid  
---------+-------
 testdb  | 16384
(1 row)
SELECT relname, oid FROM pg_class WHERE relname = 'film';
relname |  oid  
---------+-------
 film    | 16631
(1 row)
SELECT pg_relation_filepath('film');
pg_relation_filepath 
----------------------
 base/16384/16631
(1 row)
SELECT 'film'::regclass::oid;
oid  
-------
 16631
(1 row)

2.5.2 对象标识符(Object Identifiers, OID) 数值大小

由于对象标识符(Object Identifiers, OID) 是无符号的 4 字节(bytes)整数类型。所以OID 的最大大小为 42亿,计算公式如下:

  1. 232 =4294967295=42 亿

2.6 数据库模版

PostgreSQL 数据库默认提供两个模版数据库:
template0:为非默认的模板数据库,不可以修改。
template1: 为默认的模板数据库,可以修改。创建新数据库时,将会根据此数据库为模版创建。

3 数据库集簇的物理结构

3.1 数据库集簇

数据库集簇从物理结构来说本质上就是一个文件目录。称之为 数据库集簇目录(data directory)。此目录包含着一系列子目录与文件。

执行 initdb 命令之后会在 指定目录 下创建 数据集簇目录。从而初始化一个新的数据库集簇

通常为了方便起见将 数据集簇目录 的路径配置到环境变量 PGDATA 中。

下图展示了一个 PostgreSQL 数据库集簇的例子。 base 子目录中的每一个子目录都对应一个数据库。

数据库中每个表和索引都会在相应子目录下存储为(至少)一个文件;还有几个包含特定数据的子目录,以及配置文件。虽然 PostgreSQL 支持 表空间 ,但该术语的含义与其他 RDBMS 不同。 PostgreSQL 中的表空间对应一个包含基础目录之外数据的目录。

database cluster
database cluster
base 目录:$PGDATA
base 目录:$PGDATA
/
/
tablesapce
tablesapce
postgresql.conf
postgresql.conf
base
base
global
global
pg_logical
pg_logical
pg_stat
pg_stat
14070/
(template0)
14070/...
1/
(template1)
1/...
pg_tblspc/
pg_tblspc/
....
....
14071/
(testdb)
14071/...
14216
emp
14216...
14073/
(samples)
14073/...
file of table ,indexes ,free-space maps, and visibility maps
file of table ,indexes ,free-space maps, and visibili...
数据库子目录
数据库子目录
pg_logical
pg_logical
Text is not SVG - cannot display

官方文档中描述了数据库集簇 的文件和目录的分布情况。下表中列出了主要的文件与子目录:

基本目录下的数据库文件和子目录的布局(参考官方文档)

文件 描述
PG_VERSION 包含PostgreSQL主版本号
pg_hba.conf 控制PosgreSQL客户端认证
pg_ident.conf 控制PostgreSQL用户名映射
postgresql.conf 配置参数
postgresql.auto.conf 存储使用ALTER SYSTEM修改的配置参数(9.4或更新版本)
postmaster.opts 记录服务器上次启动的命令行选项
子目录 描述
base/ default 表空间的物理目录
global/ global 表空间的物理目录。
pg_commit_ts/ 事务提交的时间戳数据(9.5及更新版本)。

3.2 自定义表空间

PostgreSQL中的表空间(Tablespace)base 目录之外的附加数据区域。在PostgreSQL 8.0版本中引入了该功能。

下图展示了表空间的内部布局,以及表空间与主数据区域的关系。

database cluster
database cluster
base 目录:$KINGBASE_DATA
base 目录:$KINGBASE_DATA
/
/
symbolic link
symbolic link
tablespace
tablespace
(new_tblspc)
(new_tblspc)
/home/postgres/tblspc
/home/postgres/tblspc
base/....sys_tblspc
base/....sys_tblspc
16386
(new_tblspc)
16386...
POSTGRES_VERSION/
POSTGRES_VERSION/
16384/
(sampledb)
16384/...
18740
(sampletbl)
18740...
18740
(sampletbl)
18740...
16387/
newdb
16387/...
Text is not SVG - cannot display

执行 CREATE TABLESPACE 语句会在指定的目录下创建表空间。而在该目录下还会创建版本特定的子目录(例如 PG_16_202307071)。版本特定的命名方式为:

PG_主版本号_目录版本号

举个例子,如果在 /home/postgres/tblspc 中创建一个表空间 new_tblspc,其oid为 304152,则会在表空间下创建一个名如 PG_16_202307071 的子目录。

#postgres>
mkdir -p /home/postgres/tblspc/
psql testdb postgres 
CREATE TABLESPACE new_tblspc LOCATION '/home/postgres/tblspc'; 
\db+
List of tablespaces
    Name    |  Owner   |       Location        | Access privileges | Options |  Size   | Description 
------------+----------+-----------------------+-------------------+---------+---------+-------------
 new_tblspc | postgres | /home/postgres/tblspc |                   |         | 0 bytes | 
 pg_default | postgres |                       |                   |         | 5097 MB | 
 pg_global  | postgres |                       |                   |         | 669 kB  | 
(3 rows)
#postgres>
ls -l /home/postgres/tblspc/
drwx------ 2 postgres postgres 6 Jul 10 16:06 PG_16_202307071

表空间目录通过pg_tblspc子目录中的符号链接寻址,链接名称与表空间的OID值相同。

#postgres>
ls -l $PGDATA/pg_tblspc/
lrwxrwxrwx 1 postgres postgres 21 Jul 10 16:06 304152 -> /home/postgres/tblspc

如果在该表空间下创建新的数据库(OID304153),则会在版本特定的子目录下创建相应目录。

#postgres>
psql -U postgres -d testdb 
 CREATE  DATABASE  new_db TABLESPACE  new_tblspc ;
#postgres>
ls -l /home/postgres/tblspc/PG_16_202307071/
total 28
drwx------ 2 postgres postgres 12288 Jul 10 16:13 304153

如果在该表空间内创建一个新表,但新表所属的数据库却创建在基础目录下,那么 PostgreSQL 会首先在版本特定的子目录下创建名称与现有数据库OID相同的新目录,然后将新表文件放置在刚创建的目录下。

CREATE TABLE newtbl(id int) TABLESPACE new_tblspc;
SELECT pg_relation_filepath('newtbl');
             pg_relation_filepath             
----------------------------------------------
 pg_tblspc/304152/PG_16_202307071/16384/304154
Note

为什么需要在数据库集簇目录下添加链接文件?
为了备份和管理的时候方便管理。如果没有链接文件将会导致数据文件的混乱。

3.3 堆表数据文件内部物理结构

在数据文件(堆表,索引,也包括 FSM 文件和 VM 文件)内部,它被划分为固定长度的页(pages),或称之为 区块(blocks),大小默认为 8192 字节 (8KB)

每个文件中的页从 0 开始按顺序编号,这些数字称为区块号(block numbers)。如果当前数据块已填满,PostgreSQL 通过在数据文件末尾添加一个新的空数据块用于扩展文件。

page
8192(byte)
page
8192(byte)
page
8192(byte)
block number
0th
1th
N-th
heap tuples
(record data)
table file
pd_lsn
pd_checmsum
pd_flags
pd_lower
pd_upper
pd_special
pd_pagesize_
version
pg_prune_xid
1
2
tuple 1
tuple 2
line pointers
free space
(hole)
pd_lower
pd_upper
lp_len
lp_flags
lp_off
2bits
15 bits
15bits
偏移量
8152bits
xmin
4 bytes
xmax
4bytes
cid/_xvac
4bytes
ctid
6bytes
infomask2
2bytes
infomask
2bytes
hoff
1bytes
24bytes
偏移量
实际数据
15bytes
1bytes
24bytes
8bytes
2bytes
2bytes
2bytes
2bytes
2bytes
2bytes
4bytes
4bytes
39bytes

表的页面包含了三种类型的数据:

  1. 堆元组(heap tuples)
    堆元组就是数据记录本身。它们从页面底部开始依序增长和堆叠。并发控制WAL 日志 会介绍元组的内部结构,这一知识对于理解 PostgreSQL 并发控制WAL 日志机制是必须的。

  2. 行指针(line pointer) —— 每个行指针占4个字节,保存着指向堆元组的指针。它们也被称为项目指针(item pointer)。行指针简单地组织为一个数组,扮演了元组索引的角色。每个索引项从1开始依次编号,称为偏移号(offset number)。当向页面中添加新元组时,一个相应的新行指针也会被放入数组中,并指向新添加的元组。

  3. 首部数据(header data) —— 页面的起始位置分配了由结构PageHeaderData定义的首部数据。它的大小为24个字节,包含关于页面的元数据。该结构的主要成员变量为:

    • pd_lsn —— 本页面最近一次变更所写入XLOG记录对应的LSN。它是一个8字节无符号整数,与WAL机制相关,第9章将详细展开。
    • pd_checksum —— 本页面的校验和值。(注意只有在9.3或更高版本才有此变量,早期版中该字段用于存储页面的时间线标识)
    • pd_lowerpd_upper —— pd_lower指向行指针的末尾,pd_upper指向最新堆元组的起始位置。
    • pd_special —— 在索引页中会用到该字段。在堆表页中它指向页尾。(在索引页中它指向特殊空间的起始位置,特殊空间是仅由索引使用的特殊数据区域,包含特定的数据,具体内容依索引的类型而定,如B树,GiST,GiN等。
    /* @src/include/storage/bufpage.h */
    
    /*
     * 磁盘页面布局
     *
     * 对任何页面都适用的通用空间管理信息
     *
     *        pd_lsn        - 本页面最近变更对应xlog记录的标识。
     *        pd_checksum - 页面校验和
     *        pd_flags    - 标记位
     *        pd_lower    - 空闲空间开始位置
     *        pd_upper    - 空闲空间结束位置
     *        pd_special    - 特殊空间开始位置
     *        pd_pagesize_version - 页面的大小,以及页面布局的版本号
     *        pd_prune_xid - 本页面中可以修剪的最老的元组中的XID.
     *
     * 缓冲管理器使用LSN来强制实施WAL的基本规则:"WAL需先于数据写入"。直到xlog刷盘位置超过
     * 本页面的LSN之前,不允许将缓冲区的脏页刷入磁盘。
     *
     * pd_checksum 存储着页面的校验和,如果本页面配置了校验。0是一个合法的校验和值。如果页面
     * 没有使用校验和,我们就不会设置这个字段的值;通常这意味着该字段值为0,但如果数据库是从早于
     * 9.3版本从 pg_upgrade升级而来,也可能会出现非零的值。因为那时候这块地方用于存储页面最后
     * 更新时的时间线标识。 注意,并没有标识告诉你页面的标识符到底是有效还是无效的,也没有与之关
     * 联的标记为。这是特意设计成这样的,从而避免了需要依赖页面的具体内容来决定是否校验页面本身。
     *
     * pd_prune_xid是一个提示字段,用于帮助确认剪枝是否有用。目前对于索引页没用。
     *
     * 页面版本编号与页面尺寸被打包成了单个uint16字段,这是有历史原因的:在PostgreSQL7.3之前
     * 并没有页面版本编号这个概念,这样做能让我们假装7.3之前的版本的页面版本编号为0。我们约束页面
     * 的尺寸必须为256的倍数,留下低8位用于页面版本编号。
     *
     * 最小的可行页面大小可能是64字节,能放下页的首部,空闲空间,以及一个最小的元组。当然在实践中
     * 肯定要大得多(默认为8192字节),所以页面大小必需是256的倍数并不是一个重要限制。而在另一端,
     * 我们最大只能支持32KB的页面,因为 lp_off/lp_len字段都是15bit。
     */
    typedef struct PageHeaderData
    {
        PageXLogRecPtr     pd_lsn;            /* 最近应用至本页面XLog记录的LSN */
        uint16            pd_checksum;    /* 校验和 */
        uint16              pd_flags;        /* 标记位,详情见下 */
        LocationIndex     pd_lower;        /* 空闲空间起始位置 */
        LocationIndex     pd_upper;        /* 空闲空间终止位置 */
        LocationIndex     pd_special;        /* 特殊用途空间的开始位置 */
        uint16              pd_pagesize_version;
        TransactionId     pd_prune_xid;     /* 最老的可修剪XID, 如果没有设置为0 */
        ItemIdData        pd_linp[FLEXIBLE_ARRAY_MEMBER]; /* 行指针的数组 */
    } PageHeaderData;
    
    
    /* 缓冲区页中的项目指针(item pointer),也被称为行指针(line pointer)。
     *
     * 在某些情况下,项目指针处于 “使用中”的状态,但在本页中没有任何相关联的存储区域。
     * 按照惯例,lp_len == 0 表示该行指针没有关联存储。独立于其lp_flags的状态. 
     */
    typedef struct ItemIdData
    {
        unsigned    lp_off:15,        /* 元组偏移量 (相对页面起始处) */
                    lp_flags:2,        /* 行指针的状态,见下 */
                    lp_len:15;        /* 元组的长度,以字节计 */
    } ItemIdData;
    
    /* lp_flags有下列可能的状态,LP_UNUSED的行指针可以立即重用,而其他状态的不行。 */
    #define LP_UNUSED        0        /* unused (lp_len必需始终为0) */
    #define LP_NORMAL        1        /* used (lp_len必需始终>0) */
    #define LP_REDIRECT        2        /* HOT 重定向 (lp_len必需为0) */
    #define LP_DEAD            3        /* 死元组,有没有对应的存储尚未可知 */
    

行指针的末尾与最新元组起始位置之间的空余空间称为空闲空间(free space)空洞(hole)

为了识别表中的元组,数据库内部会使用元组标识符(tuple identifier, TID)。TID由一对值组成:元组所属页面的区块号,及指向元组的行指针的偏移号。TID的一种典型用途是索引。

Warning

结构体 PageHeaderData 定义于 src/include/storage/bufpage.h 中。

此外,大小超过约2KB(8KB的四分之一)的堆元组会使用一种称为 TOAST(The Oversized-Attribute Storage Technique,超大属性存储技术) 的方法来存储与管理。

  1. 数据字典默认为小写存储

3.4 VM 和FSM

fsm : free space map 
vm : visualbel map 

4 元组的读写方式

4.1 写入堆元组

让我们假设有一个表,仅由一个页面组成,且该页面只包含一个堆元组。 此页面的pd_lower指向第一个行指针,而该行指针和pd_upper都指向第一个堆元组。 如图1.5(a)所示。

当第二个元组被插入时,它会被放在第一个元组之后。第二个行指针被插入到第一个行指针的后面,并指向第二个元组。 pd_lower更改为指向第二个行指针,pd_upper更改为指向第二个堆元组,如图1.5(b)。页面内的首部数据(例如pd_lsn,pg_checksum,pg_flag)也会被改写为适当的值,细节在第5.3节和第9章中描述。

4.2 读取堆元组

5 数据块、wal段、数据文件大小修改

数据块按8K划分(—with-blocksize,单位为K,默认为8K)
WAL块尺寸按8k划分(—with-wal-blocksize,单位为K,默认为8K)
数据文件按1G一个分解(—with-segsize,单位为G,默认为1G)
WAL段尺寸按16MB划分(—with-wal-segsize,单位为K,默认为16MB)
重新编译

#postgresql>
./configure —with-blocksize=8—with-wal-blocksize=8—with-segsize=1 —with-wal-segsize=16  


查看编译信息

#postgresql>
pg_config —configure