暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

PostgreSQL内核之数据库集群、数据库、表(序列一)

周波 2025-07-12
927

一、前言

  PostgreSQL 是一个设计精良、开源的多用途关系型数据库系统,在全球范围内被广泛使用。
  它是一个庞大的系统,由多个集成的子系统组成,每个子系统都具有特定的复杂功能,并且彼此协同工作。尽管理解其内部机制对于 PostgreSQL 的管理和集成至关重要,但其庞大性和复杂性使得这一任务变得困难。

二、数据库集群(database cluster)、数据库(databases)和表(tables)

1.数据库集群的逻辑结构

  数据库集群是由 PostgreSQL 服务器管理的一组数据库的集合。如果你是第一次听到这个定义,可能会感到疑惑。在 PostgreSQL 中,“数据库集群”这一术语并不意味着“一组数据库服务器”。PostgreSQL 服务器运行在单个主机上,并管理一个数据库集合。

  图 1.1 展示了数据库集群的逻辑结构。数据库是数据库对象的集合。在关系数据库理论中,数据库对象是用于存储或引用数据的数据结构。(堆)表是一个典型的例子,除此之外还有许多其他对象,例如索引、序列、视图、函数等。在 PostgreSQL 中,数据库本身也是数据库对象,并且在逻辑上彼此分离。所有其他数据库对象(例如表、索引等)都属于各自的数据库。
image.png
  PostgreSQL 中的所有数据库对象在内部都由各自的对象标识符(OID)管理,这些标识符是无符号的 4 字节整数。数据库对象与其对应 OID 之间的关系存储在相应的系统目录中,具体存储位置取决于对象的类型。例如,数据库和堆表的 OID 分别存储在 pg_database 和 pg_class 中。
  要查找所需对象的 OID,可以执行以下查询:

sampledb=# SELECT datname, oid FROM pg_database WHERE datname = 'sampledb';
 datname  |  oid
----------+-------
 sampledb | 16384
(1 row)

sampledb=# SELECT relname, oid FROM pg_class WHERE relname = 'sampletbl';
  relname  |  oid
-----------+-------
 sampletbl | 18740
(1 row)

2.数据库集群的物理结构

  数据库集群本质上是一个单独的目录,称为基目录(base directory)。它包含一些子目录和许多文件。当你执行 initdb 实用程序来初始化一个新的数据库集群时,会在指定目录下创建一个base目录。base目录的路径通常设置为环境变量 PGDATA。
  图 1.2 展示了 PostgreSQL 中数据库集群的一个示例。数据库是base目录下的一个子目录,而每个表和索引(至少)是一个文件,存储在其所属数据库的子目录下。此外,还有一些子目录包含特定的数据和配置文件。
  虽然 PostgreSQL 支持表空间(tablespace),但该术语的含义与其他关系数据库管理系统(RDBMS)不同。在 PostgreSQL 中,表空间是一个单独的目录,包含base目录之外的一些数据。
image.png

  在以下小节中,将介绍 PostgreSQL 中数据库集群的布局、数据库、与表和索引相关的文件以及表空间的内容。

(1)数据库集群的布局

  数据库集群的布局已在官方文档(https://www.postgresql.org/docs/current/storage-file-layout.html)中进行了描述。
  本文档中列出的主要文件和子目录如表 1.1 所示:

files description
PG_VERSION 一个包含 PostgreSQL 主版本号的文件
current_logfiles 用于记录由日志收集器当前写入的日志文件的文件
pg_hba.conf 用于控制 PostgreSQL 客户端认证的文件。
pg_ident.conf 用于控制 PostgreSQL 用户名映射的文件
postgresql.conf 用于设置配置参数的文件。
postgresql.auto.conf 用于存储通过 ALTER SYSTEM 设置的配置参数的文件。(版本 9.4 或更高)
postmaster.opts 记录服务器上次启动时使用的命令行选项的文件。
subdirectories description
base/ 包含每个数据库子目录的子目录。
global/ 包含集群范围表的子目录,例如 pg_database 和 pg_control。
pg_commit_ts/ 包含事务提交时间戳数据的子目录。(版本 9.5 或更高)
pg_clog/ (versions 9.6 or earlier) 包含事务提交状态数据的子目录。在版本 10 中更名为 pg_xact。
pg_dynshmem/ 包含动态共享内存子系统使用的文件的子目录。(版本 9.4 或更高)
pg_logical/ 包含逻辑解码状态数据的子目录。(版本 9.4 或更高)
pg_multixact/ 包含多事务状态数据的子目录。(用于共享行锁)
pg_notify/ 包含 LISTEN/NOTIFY 状态数据的子目录。
pg_repslot/ 包含复制槽数据的子目录。(版本 9.4 或更高)
pg_serial/ 包含已提交可序列化事务信息的子目录。
pg_snapshots/ 包含导出快照的子目录。PostgreSQL 的函数 pg_export_snapshot 在此子目录中创建快照信息文件。(版本 9.2 或更高)
pg_stat/ 包含统计子系统的永久文件的子目录。
pg_stat_tmp/ 包含统计子系统的临时文件的子目录。
pg_subtrans/ 包含子事务状态数据的子目录。
pg_tblspc/ 包含指向表空间的符号链接的子目录。
pg_twophase/ 包含预备事务状态文件的子目录。
pg_wal/ (versions 10 or later) 包含 WAL(预写日志)段文件的子目录。在版本 10 中从 pg_xlog 更名为pg_wal。
pg_xact/ (versions 10 or later) 包含事务提交状态数据的子目录。在版本 10 中从
pg_xlog/ (versions 9.6 or earlier) 包含 WAL(预写日志)段文件的子目录。在版本 10 中更名为 pg_wal
(2)数据库布局

  一个数据库就是base子目录下的一个子目录。数据库目录的名称与各自的OID(对象标识符)相同。例如,当数据库“sampledb”的OID为16384时,其子目录名称就是16384。

$ cd $PGDATA
$ ls -ld base/16384
drwx------  213 postgres postgres  7242  8 26 16:33 16384
(3)与表和索引相关的文件布局

  每个大小小于1GB的表或索引都存储在其所属数据库目录下的单个文件中。表和索引在内部由各自的OID(对象标识符)进行管理,而它们的数据文件则由变量relfilenode管理。表和索引的relfilenode值通常但不总是与各自的OID相匹配,具体细节如下所述。
  例如,展示表’sampletbl’的OID和relfilenode:

sampledb=# SELECT relname, oid, relfilenode FROM pg_class WHERE relname = 'sampletbl';
  relname  |  oid  | relfilenode
-----------+-------+-------------
 sampletbl | 18740 |       18740
(1 row)

  在这种情况下,oid 和 relfilenode 的值是相等的。此外,表 ‘sampletbl’ 的数据文件路径为 base/16384/18740:

$ cd $PGDATA
$ ls -la base/16384/18740
-rw------- 1 postgres postgres 8192 Apr 21 10:21 base/16384/18740

  表和索引的 relfilenode 值可能会因执行某些命令而改变,例如 TRUNCATE、REINDEX、CLUSTER。例如,如果我们对表 ‘sampletbl’ 执行 TRUNCATE 操作,PostgreSQL 会为该表分配一个新的 relfilenode(例如 18812),删除旧的数据文件(18740),并创建一个新的数据文件(18812)。

sampledb=# TRUNCATE sampletbl;
TRUNCATE TABLE

sampledb=# SELECT relname, oid, relfilenode FROM pg_class WHERE relname = 'sampletbl';
  relname  |  oid  | relfilenode
-----------+-------+-------------
 sampletbl | 18740 |       18812
(1 row)
Tips
  在 9.0 或更高版本中,内置函数 pg_relation_filepath() 非常有用,因为该函数可以返回具有指定 OID 或名称的关系(如表或索引)的文件路径名。
sampledb=# SELECT pg_relation_filepath('sampletbl');
 pg_relation_filepath
----------------------
 base/16384/18812
(1 row)

  当表或索引的文件大小超过 1GB 时,PostgreSQL 会创建一个名为 relfilenode.1 的新文件并使用它。如果新文件也被填满,PostgreSQL 将继续创建另一个名为 relfilenode.2 的文件,依此类推。
$ cd $PGDATA
$ ls -la -h base/16384/19427*
-rw------- 1 postgres postgres 1.0G  Apr  21 11:16 data/base/16384/19427
-rw------- 1 postgres postgres  45M  Apr  21 11:20 data/base/16384/19427.1
Tips
  表或索引的最大文件大小可以通过在构建 PostgreSQL 时使用配置选项 –with-segsize 来更改。

  通过检查数据库子目录可以发现,每个表都有两个关联的文件,后缀分别为 _fsm 和 _vm。它们分别称为空闲空间映射表(Free Space Map)和可见性映射表(Visibility Map)。

  • 空闲空间映射表 存储了表文件中每个页面的空闲空间信息。
  • 可见性映射表 存储了表文件中每个页面的可见性信息。

  索引只有单独的空闲空间映射表,而没有可见性映射表。
  以下是一个具体的示例:

$ cd $PGDATA
$ ls -la base/16384/18751*
-rw------- 1 postgres postgres  8192 Apr 21 10:21 base/16384/18751
-rw------- 1 postgres postgres 24576 Apr 21 10:18 base/16384/18751_fsm
-rw------- 1 postgres postgres  8192 Apr 21 10:18 base/16384/18751_vm

  空闲空间映射表和可见性映射表在内部也可能被称为每个关系(relation)的分支(forks)。

  • 空闲空间映射表是表/索引数据文件的第一个分支(分支编号为 1),
  • 可见性映射表是表数据文件的第二个分支(分支编号为 2)。
  • 数据文件本身的分支编号为 0。
(4)表空间

  在 PostgreSQL 中,表空间(tablespace)是base目录之外的额外数据区域。此功能是在 8.0 版本中实现的。
  图 1.3 展示了表空间的内部布局及其与主数据区域的关系。
image.png
  表空间是在执行 CREATE TABLESPACE 语句时指定的目录中创建的。在该目录下,会创建一个与版本相关的子目录(例如 PG_14_202011044)。版本相关子目录的命名规则如下所示:

PG _ 'Major version' _ 'Catalogue version number'

  创建表空间:

CREATE TABLESPACE tablespace_name
    [ OWNER { new_owner | CURRENT_ROLE | CURRENT_USER | SESSION_USER } ]
    LOCATION 'directory'
    [ WITH ( tablespace_option = value [, ... ] ) ]

  例如,在 /home/postgres/tblspc 目录下创建一个名为 new_tblspc 的表空间,其 OID 为 16386,此时会在表空间目录下创建一个名为 PG_14_202011044 的子目录。

$ ls -l /home/postgres/tblspc/
total 4
drwx------ 2 postgres postgres 4096 Apr 21 10:08 PG_14_202011044

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

$ ls -l $PGDATA/pg_tblspc/
total 0
lrwxrwxrwx 1 postgres postgres 21 Apr 21 10:08 16386 -> /home/postgres/tblspc

  当在表空间中创建一个新数据库(OID 为 16387)时,其目录会被放置在版本相关的子目录下:

$ ls -l /home/postgres/tblspc/PG_14_202011044/
total 4
drwx------ 2 postgres postgres 4096 Apr 21 10:10 16387

  如果在位于base目录中的数据库内创建了一个新表,首先会在版本相关的子目录下创建一个新目录,该目录的名称根据数据库的 OID 命名。然后,新表文件会被放置在这个新创建的目录中:

sampledb=# CREATE TABLE newtbl (.....) TABLESPACE new_tblspc;

sampledb=# SELECT pg_relation_filepath('newtbl');
             pg_relation_filepath
---------------------------------------------
 pg_tblspc/16386/PG_14_202011044/16384/18894

3.堆表文件的内部布局

  在数据文件(堆表、索引、空闲空间映射和可见性映射)中,文件被划分为固定长度的页(或块),默认情况下为8192字节(8 KB)。每个文件中的页从0开始按顺序编号,这些编号称为块号。如果文件已满,PostgreSQL会在文件末尾添加一个新的空页以增加文件大小。
  页面的内部布局取决于数据文件的类型。在本节中,将描述表的布局。
image.png

  表中的页面包含三种数据:

  • (1)堆元组(heap tuple)
    堆元组是记录数据本身。堆元组从页面的底部开始依次堆叠。
    元组的内部结构将在第5.2节和第9章中描述,因为这需要了解PostgreSQL中的并发控制(CC)和预写日志(WAL)机制。

  • (2)行指针(line pointer)
    行指针长度为4字节,保存指向每个堆元组的指针。它也被称为项指针(item pointer)。
    行指针形成一个简单的数组,充当元组的索引。每个索引从1开始顺序编号,称为偏移号(offset number)。当向页面添加新元组时,一个新的行指针也会被推入数组,以指向新元组。

  • (3)头部数据(header data)
    在页面开头分配了由PageHeaderData结构定义的头部数据。它占24字节,包含页面的通用信息。
    该结构的主要变量如下:
    pd_lsn
    该变量存储由该页面最后一次更改写入的XLOG记录的LSN(日志序列号)。它是一个8字节的无符号整数,与WAL(预写日志)机制相关。详细信息在第9.1.2节中描述。
    pd_checksum
    该变量存储该页面的校验和值。(注意,该变量在9.3或更高版本中支持;在早期版本中,这部分存储的是pd_tli,即页面的时间线ID。)
    pd_lower 和 pd_upper
    pd_lower指向行指针的末尾,pd_upper指向最新堆元组的起始位置。
    pd_special
    该变量用于索引。在表的页面中,它指向页面的末尾。(在索引的页面中,它指向特殊空间(special space)的起始位置,这是仅由索引持有的数据区域,包含根据索引类型(如B树、GiST、GiN等)特定的数据。)

  行指针末尾和最新元组起始位置之间的空白空间称为空闲空间(free space)或空洞(hole)。
  为了标识表中的元组,内部使用元组标识符(TID)。TID由一对值组成:包含该元组的页面的块号,以及指向该元组的行指针的偏移号。它的典型用法是在索引中。
  在计算机科学领域,这种类型的页面被称为分槽页面(slotted page),而行指针对应于槽数组(slot array)。
  此外,大小超过约2 KB(约8 KB的1/4)的堆元组会使用一种称为TOAST(The Oversized-Attribute Storage Technique,超大属性存储技术)的方法进行存储和管理。详细信息可以参考PostgreSQL的官方文档https://www.postgresql.org/docs/current/storage-toast.html。

4.读写元组的方法

(1)写入堆元组

  假设一个表由一个页面组成,且该页面仅包含一个堆元组。该页面的pd_lower指向第一个行指针,而行指针和pd_upper都指向第一个堆元组。如图1.5(a)所示。
  当插入第二个元组时,它会被放置在第一个元组之后。第二个行指针被追加到第一个行指针之后,并指向第二个元组。pd_lower会更新为指向第二个行指针,而pd_upper会更新为指向第二个堆元组。如图1.5(b)所示。页面中的其他头部数据(例如pd_lsn、pg_checksum、pg_flag等)也会被更新为适当的值;
image.png

(2)读取堆元组

  这里概述了两种典型的访问方法:顺序扫描和B树索引扫描:

  • 顺序扫描(Sequential scan)
    它通过依次扫描每个页面中的所有行指针,顺序读取所有页面中的所有元组。如图1.6(a)所示。

  • B树索引扫描(B-tree index scan)
    它读取包含索引元组的索引文件,每个索引元组由一个索引键和一个指向目标堆元组的TID(元组标识符)组成。
    如果找到了包含目标键的索引元组,PostgreSQL会使用获取的TID值读取所需的堆元组。
    例如,在图1.6(b)中,获取的索引元组的TID值为(block = 7, Offset = 2)。这意味着目标堆元组是表中第7页的第2个元组,因此PostgreSQL可以直接读取所需的堆元组,而无需在页面中进行不必要的扫描。
    image.png
      PostgreSQL 还支持 TID 扫描(TID-Scan)、位图扫描(Bitmap-Scan) 和 仅索引扫描(Index-Only-Scan)。

  TID 扫描 是一种通过使用目标元组的 TID 直接访问元组的方法。例如,要查找表中第 0 页的第 1 个元组,可以发出以下查询:

sampledb=# SELECT ctid, data FROM sampletbl WHERE ctid = '(0,1)';
 ctid  |   data
-------+-----------
 (0,1) | AAAAAAAAA
(1 row)

  本文译自https://www.interdb.jp/pg/pgsql01.html,更多详细信息请参考原文。

最后修改时间:2025-07-14 09:53:27
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论