一、逻辑存储结构
逻辑存储结构通常指表、索引、视图、函数等逻辑对象(也称数据库对象)

参考之前写的文章数据库对象
二、物理存储结构
物理存储结构表示数据库在物理层面上是如何存储的。
在初始化initdb数据库集簇(database cluster)时会在指定目录下创建基础目录(即通常所说的数据目录,会被配置到环境变量PGDATA中)。所有相关组成数据库的物理文件的分布:


| 子目录/文件 | 描述 |
|---|---|
| base | 默认的数据库目录 |
| global | 数据库集簇中的所有表 |
| pg_commmit_ts | 事务提交的时间戳目录 |
| pg_dynshmem | 动态共享内存目录 |
| pg_logical | 逻辑解码的状态数据 |
| pg_multixact | 多个事务的状态目录 |
| pg_notify | 包含系统发出的 LISTEN/NOTIFY 的目录 |
| pg_replslot | 复制槽目录 |
| pg_serial | 已提交的可序列化事务的信息目录 |
| pg_snapshots | 快照目录(pg_export_snapshot函数导出的快照信息文件) |
| pg_stat | 统计目录 |
| pg_stat_tmp | 临时统计目录 |
| pg_subtrans | 子事务的状态数据 |
| pg_tblspc | 各个用户自建表空间指向的软链接符号目录 |
| pg_twophase | 两阶段提交目录 |
| pg_wal | wal segment 目录 |
| pg_xact | 事务提交状态的目录 |
| pg_hba.conf | 基于主机的认证配置文件,用于配置允许哪些IP的主机通过哪种方式访问数据库 |
| pg_ident.conf | 基于主机的映射文件 |
| PG_VERSION | 包含PostgreSQL主版本号 |
| postgresql.auto.conf | 存储使用 ALTER SYSTEM修改的配置参数 |
| postgresql.conf | 默认的数据库参数文件 |
| postmaster.opts | 记录服务器上次启动时的相关密令信息 |
| postmaster.pid | 当前数据库运行的进程文件 |
下面介绍一些常用的的目录及文件
1、base 目录
1.1 概述
base目录存储用户创建的数据库文件,及隶属于用户数据库的所有关系,比如表、索引等
1.2 一级目录
以数据库oid命名的目录

postgres=# SELECT oid,datname from pg_database;
oid | datname
-------+------------
13892 | postgres
16385 | lmj_test -- 自己建的数据库
1 | template1
13891 | template0
16408 | db_new_tsp -- 在自建的表空间里的数据库(下文有详细介绍)
(5 rows)
1.3 二级文件
存储某个数据库内的所有relation,包括表、索引、视图等。
二级子文件分为三大类:
- 以
oid命名的主数据文件; - 以
_fsm结尾的空闲空间映射文件; - 以
_vm结尾的可见性映射文件。

-- 以 上图中的 1247 为例:1247是普通表 pg_type 的oid
postgres=# select oid,relname,relkind from pg_class where oid = 1247;
oid | relname | relkind
------+---------+---------
1247 | pg_type | r
(1 row)
--- relkind 值为 r:表示普通表
-
主数据文件(具体介绍请看第三节
堆表文件的内部布局) -
FSM(FREE SPACE MAP) - 空闲空间映射文件
-
存储相应表或索引文件中每个页面上的空闲空间信息。hash索引除外
-
详情可查看
src/backend/storage/freespace/README -
可使用扩展
pg_freespace查看特定表或索引上的空闲空间信息。lmj_test=> \c lmj_test postgres You are now connected to database "lmj_test" as user "postgres". lmj_test=# create extension pg_freespacemap ; CREATE EXTENSION lmj_test=# create table tab_fsm(id int,name varchar); CREATE TABLE lmj_test=# insert into tab_fsm select id,md5(id::varchar) from generate_series(1,10000) as id; INSERT 0 10000 lmj_test=# select *,round(100*avail/8192 ,2) as "freespace ratio" from pg_freespace('tab_fsm') where avail !=0; blkno | avail | freespace ratio -------+-------+----------------- 83 | 5440 | 66.00 (1 row)详情可查看官网:pg_freespacemap扩展
-
-
VM(VISIBILITY MAP)可见性映射文件
-
存储页面的可见性信息,包含所有活跃事务的元组,也包含需要冻结的一些元组
-
可使用扩展
pg_freespace查看lmj_test=# create extension pg_visibility ; CREATE EXTENSION lmj_test=# create table tab_vm as select * from tab_fsm ; SELECT 10000 -- 删除前 lmj_test=# select * from pg_visibility_map('tab_vm') where blkno = 0; blkno | all_visible | all_frozen -------+-------------+------------ 0 | t | f -- 删除 id<100 的元组 lmj_test=# delete from tab_vm where id<100; DELETE 99 -- 删除后重新查看:all_visible t-->f,此时元组不可见 lmj_test=# select * from pg_visibility_map('tab_vm') where blkno = 0; blkno | all_visible | all_frozen -------+-------------+------------ 0 | f | f (1 row)详情可查看 pg_visibility扩展
-
每一个堆元组都存储两位:如果第一位被设置,那么表示页面都是可见的,既然可见,那么该页面不包含任何清理的元组信息。索引扫描可以使用索引元组及逆行查询。如果第二位被设置,那么vacuum将会跳过所有的页面访问,提高vacuum的速度。
/* src/backend/access/heap/visibilitymap.c */
The visibility map is a bitmap with two bits (all-visible and all-frozen)per heap page.
- A set all-visible bit means that all tuples on the page areknown visible to all transactions, and therefore the page doesn’t need tobe vacuumed.
- A set all-frozen bit means that all tuples on the page arecompletely frozen, and therefore the page doesn’t need to be vacuumed evenif whole table scanning vacuum is required (e.g. anti-wraparound vacuum).The all-frozen bit must be set only when the page is already all-visible.
-
索引没有可见性映射文件,只有空闲空间映射文件
-
2、global 目录
2.1 概述
存储pg_control及数据库集群维度的数据库及其关系,非客户维度的数据

lmj_test=# select oid,relname,relkind from pg_class where oid = 6115 ;
oid | relname | relkind
------+-------------------------------+---------
6115 | pg_subscription_subname_index | i
(1 row)
2.2 pg_control文件
pg_control文件记录数据库集群控制信息,包括initdb初始化、WAL和checkpoint的信息
3、pg_wal 目录
3.1 概述
pg_wal是WAL机制中的wal日志存储目录
| PG 版本 | 目录名 |
|---|---|
| <10 | pg_xlog |
| >=10 | pg_wal |
3.2 WAL(write ahead log) 机制
数据库重做日志。从名字上看也可以简单的理解为,在数据写入磁盘前先记录日志。
3.3 文件结构


-
wal segment 文件(wal段文件)
-
存储着数据库行记录明细,每一条记录明细都是服务于数据库恢复操作的,确保前后数据一致。首先针对数据的任意一次修改操作均被记录在wal段文件中,包括insert、update和delete,其次系统的一些管理行为也会被记录在wal段文件中,例如事务提交和vacuum等行为。
-
文件名:26个十六进制数由三部分组成


LSN(Log Sequnce Number)日志序列号:记录 wal 日志的绝对位置,是一个不断增长的8字节(64 bit )的长数字
-
注意区分数据库log日志文件和wal文件。从内容上:数据库日志是记录数据库层面上的操作,比如记录启动数据库等操作(记录日志的内容可自行定义),而针对数据的任意一次修改操作均被记录在wal段文件中。从作用上:数据库日志-当系统发生错误时可以从 log 中查找 error,并解决问题;而wal日志用于数据库备份恢复及流复制等,与Oracle中的Redo Log功能相同。
-
文件大小:默认16MB,可以在
initdb时通过--wal-segments选项来配置wal文件大小
-
-
.history
.history文件内容包括原.history文件,当前时间线切换记录和切换原因,作用于数据库的时间点恢复行为。当数据库引擎从多个时间线的备份中恢复时,数据库从.history文件中找到从pg_control的start_timeline到指定的recovery_target_timeline间的所有wal段文件进行恢复。
-
archive_status 目录
archive_status是wal段文件的备份目录,包括.ready和.done文件。超出wal_keep_segments数目限制的wal日志会在archive_status目录内被标记,归档操作完成后被进一步移除。
-
.ready
.ready是同名wal段文件在archive_status目录内的标记文件,代表该wal段文件可被归档。wal段文件在数据目录中的存储文件数量是有上限的,可以通过wal_keep_size 参数来约束,因此数据库引擎在wal段文件个数达到上限后会在archive_status目录内增加可移除的wal段文件的标记文件,文件名是原wal段文件名后增加.ready后缀,等待归档工具进行归档。
-
.done
.done是同名wal段文件在archive_status目录内的标记文件,代表该wal段文件已被归档,可以被清理。数据库引擎默认通过archive_command命令对.ready文件进行归档,归档成功与否取决于archive_command命令返回true还是false,当archive_command返回true时,代表与.ready文件同名的wal段文件已被归档,引擎再将该文件的扩展名重命名为.done,等待数据库引擎在下一次的checkpoint时进一步清理原wal段文件。
-
4、pg_xact 目录
4.1 概述
事务提交日志存储目录,默认256KB
4.2 commit log

事务提交日志存储数据库的单个事务运行状态。Commit Log由共享内存中一组8KB的page组成,每个page包含一列数组,每个数组元素包含XID和该事物的实时状态。当page不足时,创建新的page来存储新的事务。
5、配置文件
-
postgresql.conf
数据库初始化时默认的配置文件
-
postgresql.auto.conf
存储使用
ALTER SYSTEM修改的配置参数,优先级别高于postgresql.conf文件 -
pg_hba.conf
pg_hba.conf文件负责客户端的连接和认证,起到防火墙的作用。格式:
TYPE DATABASE USER ADDRESS METHOD
三、表空间的布局
3.1 默认的表空间
两个默认的表空间:pg_default和pg_global
postgres=# select oid,spcname from pg_tablespace;
oid | spcname
------+------------
1663 | pg_default
1664 | pg_global
(2 rows)
postgres=# create table tab_default_tsp(id int,name varchar);
CREATE TABLE
-- reltablespace=0,表示使用的是默认表空间
postgres=# select oid,relname,relfilenode,reltablespace from pg_class where relname = 'tab_default_tsp';
oid | relname | relfilenode | reltablespace
-------+-----------------+-------------+---------------
16391 | tab_default_tsp | 16391 | 0
(1 row)
-
所有创建的数据库,如果没有明确指定表空间,那么使用默认的表空间
pg_default。用于管理整个数据库集群的表默认被存储在pg_global表空间中create database lmj_test owner lmj; postgres=# SELECT db.oid,db.datname,tsp.spcname from pg_database db inner join pg_tablespace tsp on db.dattablespace=tsp.oid where db.datname = 'lmj_test'; oid | datname | spcname -------+----------+------------ 16385 | lmj_test | pg_default (1 row) -
用于管理整个数据库集群的表默认被存储在
pg_global表空间中;pg_global表空间的物理位置为$PGDATA\global目录。 -
pg_global不能被当作默认的表空间
3.2 用户创建的表空间
执行CREATE TABLESPACE语句会在指定的目录下创建表空间,在该目录下还会创建版本特定的子目录。版本特定的命名方式为PG_主版本号_目录版本号

用户创建的表空间对应一个目录
--# 在 /soft/pg14/data6000/lmj_tsp_test 目录下创建了 lmj_tsp_test 表空间
postgres=# create tablespace lmj_tsp_test owner lmj location '/soft/pg14/data6000/lmj_tsp_test';
CREATE TABLESPACE
--# 新建的表空间对应的 oid = 16397
postgres=# select oid,spcname from pg_tablespace;
oid | spcname
-------+--------------
1663 | pg_default
1664 | pg_global
16397 | lmj_tsp_test
(3 rows)
--# 在目录下创建了版本特定的子目录-PG_14_202107181
postgres=# \! ls -l /soft/pg14/data6000/lmj_tsp_test
total 0
drwx------. 2 postgres postgres 6 Jan 20 17:20 PG_14_202107181
--# $PGDATA/pg_tblspc 目录下有与新建表空间oid相同的符号链接指向实际的表空间目录
postgres=# \! ls -l $PGDATA/pg_tblspc
total 0
lrwxrwxrwx. 1 postgres postgres 32 Jan 20 17:20 16397 -> /soft/pg14/data6000/lmj_tsp_test
--#1. 在新建的表空间 lmj_tsp_test 下创建 tab_new_tsp 表
postgres=# create table tab_new_tsp(id int,name varchar) tablespace lmj_tsp_test;
CREATE TABLE
---# tab_new_tsp 表的所属表空间为 lmj_tsp_test
postgres=# select oid,relname,relfilenode,reltablespace from pg_class where relname = 'tab_new_tsp';
oid | relname | relfilenode | reltablespace
-------+-------------+-------------+---------------
16403 | tab_new_tsp | 16403 | 16397
(1 row)
---# 使用 pg_relation_filepath 函数查找该表对应的目录
postgres=# select pg_relation_filepath('tab_new_tsp');
pg_relation_filepath
---------------------------------------------
pg_tblspc/16397/PG_14_202107181/13892/16403
(1 row)
--#2. 在新建的表空间 lmj_tsp_test 下创建 db_new_tsp 数据库
create database db_new_tsp owner lmj tablespace lmj_tsp_test;
---# 数据库对应的 oid=16408
postgres=# select oid,datname,datdba,dattablespace from pg_database where datname = 'db_new_tsp';
oid | datname | datdba | dattablespace
-------+------------+--------+---------------
16408 | db_new_tsp | 16384 | 16397
(1 row)
---# 在版本特定的子目录下创建相应的数据库目录
postgres=# \! ls -l $PGDATA/lmj_tsp_test/PG_14_202107181 | grep 16408
drwx------. 2 postgres postgres 8192 Jan 20 17:47 16408
四、表和索引相关文件的布局
4.1 数据库相关知识
-
任何的新初始化数据库都有一个统一的存放目录 base 用来存放默认的数据库:postgres、template1、template0。
postgres=# SELECT oid,datname,datdba from pg_database ; oid | datname | datdba -------+------------+-------- 13892 | postgres | 10 1 | template1 | 10 13891 | template0 | 10 16408 | db_new_tsp | 16384 (4 rows) postgres=# \! ls -l $PGDATA/base total 48 drwx------. 2 postgres postgres 8192 Jan 12 18:02 1 drwx------. 2 postgres postgres 8192 Jan 12 18:02 13891 drwx------. 2 postgres postgres 8192 Jan 20 17:26 13892 -
当创建数据库时不指定
TEMPLATE,将使用默认模板数据库template1 -
在数据库中有一个特殊的数据类型叫做
oid(object identifier)对象标识符,每个数据库都有对应的唯一的 oid 值。整个数据库结构和数据库对象都是通过oid来管理的。
4.2 表在数据库中的管理
创建一张表 tab_test,oid=16414
postgres=# \c lmj_test lmj
You are now connected to database "lmj_test" as user "lmj".
lmj_test=> create table tab_test (id int,name varchar);
CREATE TABLE
lmj_test=> select oid,relname,relfilenode from pg_class where relname = 'tab_test';
oid | relname | relfilenode
-------+----------+-------------
16414 | tab_test | 16414
(1 row)
lmj_test=> select pg_relation_filepath('tab_test');
pg_relation_filepath
----------------------
base/16385/16414
(1 row)
-
默认不会给创建的空表分配空间,只分配一个文件名称
lmj_test=> \! ls -l $PGDATA/base/16385/16414 -rw-------. 1 postgres postgres 0 Jan 20 18:24 /soft/pg14/data6000/base/16385/16414 -
在第一次执行
INSERT操作后,将会分配一个页面的大小(8K)lmj_test=> insert into tab_test values(1,'lmj'); INSERT 0 1 lmj_test=> \! ls -l $PGDATA/base/16385/16414 -rw-------. 1 postgres postgres 8192 Jan 20 18:32 /soft/pg14/data6000/base/16385/16414 -
表在内部通过
oid管理,外部通过refilenode管理注:表的 oid 不一定等于 refilenode,如下
# 对表进行 truncate 操作 lmj_test=> truncate TABLE tab_test ; TRUNCATE TABLE # 表的 oid 不等于 filenode,表的 filenode 的值发生改变 lmj_test=> select oid,relname,relfilenode from pg_class where relname = 'tab_test'; oid | relname | relfilenode -------+----------+------------- 16414 | tab_test | 16419 (1 row) # 查询表的存储位置 - 以 filenode 为名的文件 lmj_test=> select pg_relation_filepath('tab_test'); pg_relation_filepath ---------------------- base/16385/16419 (1 row) # 此时表的 oid和 filenode 的目录下的文件都为空 lmj_test=> \! ls -l $PGDATA/base/16385/16414 -rw-------. 1 postgres postgres 0 Jan 20 18:34 /soft/pg14/data6000/base/16385/16414 lmj_test=> \! ls -l $PGDATA/base/16385/16419 -rw-------. 1 postgres postgres 0 Jan 20 18:34 /soft/pg14/data6000/base/16385/16419 # 插入数据 lmj_test=> insert into tab_test values(1,'lmj'); INSERT 0 1 # 以 oid 命名的文件大小仍为0,filenode命名为文件大小为 8K lmj_test=> \! ls -l $PGDATA/base/16385/16414 -rw-------. 1 postgres postgres 0 Jan 20 18:34 /soft/pg14/data6000/base/16385/16414 lmj_test=> \! ls -l $PGDATA/base/16385/16419 -rw-------. 1 postgres postgres 8192 Jan 20 18:35 /soft/pg14/data6000/base/16385/16419 -
表默认单个文件大小为1G。如果超过1G,将会使用
relfilenode.编号(编号从1开始)来命名新的文件,直到新文件填满1G,以此类推。也可以在编译时使用--eith-segsize来指定
五、堆表文件的内部布局
5.1 堆表文件的页面布局

表的页面包含三种类型的数据:
-
堆元组(heap tuples) :堆元组就是数据记录本身。它们从页面底部开始依序堆叠
-
行指针(line pointer):每个行指针占4个字节,保存着指向堆元组的指针。它们也被称为项目指针(item pointer)。行指针简单地组织为一个数组,扮演了元组索引的角色。每个索引项从1开始依次编号,称为偏移号(offset number)。当向页面中添加新元组时,一个相应的新行指针也会被放入数组中,并指向新添加的元组。
-
首部数据(header data):页面的起始位置分配了由结构
PageHeaderData定义的首部数据。它的大小为24个字节,包含关于页面的元数据。该结构的主要成员变量为:成员变量 描述 pd_lsn 该页面最近一次变更所写入XLOG记录对应的LSN。它是一个8字节无符号整数 pd_checksum 页面的校验和 pd_flags 标记位(数据页面中的格式化的空间或未格式化的空间) pd_lower 空闲空间的起始位置 pd_upper 空间空间的结束位置 pd_special 特殊空间的偏移量 pd_pagesize_version 页面的大小以及页面的版本编号 pd_prune_xid 页面中最老元组的xid
/* @src/include/storage/bufpage.h */
在 bufpage.h 文件中查看标准的页面定义
5.2 使用 pg_filedump 工具分析
lmj_test=# select pg_relation_filepath('tab_test');
pg_relation_filepath
----------------------
base/16385/16451
(1 row)

lmj_test=# select * from tab_test ;
id | name
----+------------
1 | lmj
1 | PostgreSQL
(2 rows)
-- 显示格式化的块内容转储
[postgres@node1 pg_filedump]$ ./pg_filedump -d $PGDATA/base/16385/16451 | tail -n 10
1f90: 00000000 00000000 00000000 00000000 ................
1fa0: 00000000 00000000 00000000 00000000 ................
1fb0: 00000000 00000000 fd020000 00000000 ................
1fc0: 00000000 00000000 02000200 02091800 ................
1fd0: 01000000 17506f73 74677265 53514c00 .....PostgreSQL.
1fe0: fc020000 00000000 00000000 00000000 ................
1ff0: 01000200 02091800 01000000 096c6d6a .............lmj




