磁盘文件结构(PGDATA / relfilenode)
数据库的内存是易失的,最终所有数据都要落到磁盘上。本节拆开 PostgreSQL 的数据目录(PGDATA),讲清 base/global/pg_wal/pg_xact 等子目录分别存放什么、表和索引在磁盘上如何用 OID(对象标识符)命名、为什么一张表会变成多个 1GB 的段文件,并给出用 pg_relation_filepath() 等函数把「逻辑表」映射到「物理文件」的实操方法。
前置知识
阅读本节前,建议先了解:内存结构(Shared Buffer / WAL Buffer)
基础概念
PostgreSQL 把整个实例的所有数据都放在一个目录里,这个目录叫数据目录(Data Directory),环境变量名是 PGDATA。理解磁盘结构的关键在于:PostgreSQL 不按「表名」存文件,而是按「对象标识符」存文件。
| 术语 | 英文名 | 说明 |
|---|---|---|
| 数据目录 | PGDATA, Data Directory | 一个实例的全部数据所在目录,由 initdb 创建 |
| 对象标识符 | OID, Object Identifier | 系统为每个对象分配的 32 位无符号整数唯一 ID |
| 关系文件节点 | relfilenode | 表/索引物理文件的主文件名,通常等于其 OID,但会被重写操作改变 |
| 数据块 | Block / Page | 磁盘与内存之间读写的最小单位,默认 8KB |
| 段文件 | Segment File | 单个物理文件上限 1GB,超出后自动切分出的 .1、.2 文件 |
| 分叉文件 | Fork | 同一个 relfilenode 的旁路文件,如 _fsm、_vm、_init |
| 表空间 | Tablespace | 把数据文件放到 PGDATA 之外的其他磁盘路径 |
| 目录版本号 | Catalog Version | 编译期生成的内部版本标识,用于隔离不同大版本的物理格式 |
| 控制文件 | pg_control | 记录集群状态、最新检查点、WAL 位置等元信息的二进制文件 |
不要手动碰 PGDATA 里的任何文件
PGDATA 是 PostgreSQL 的私有领地。直接删除、重命名、复制里面的文件(尤其是 pg_wal/、pg_xact/、pg_filenode.map、global/pg_control)会导致集群无法启动甚至数据永久损坏。唯一的备份手段是 pg_basebackup 或文件系统级快照,而不是 cp -r PGDATA。
PGDATA 目录总览
一个由 initdb 初始化、跑过一段时间的实例,目录结构大致如下:
$PGDATA/
├── PG_VERSION # 主版本号,内容就是一行 "18"
├── postgresql.conf # 主配置文件
├── postgresql.auto.conf # ALTER SYSTEM 写入的配置(优先级更高)
├── pg_hba.conf # 客户端认证规则
├── pg_ident.conf # 认证用户名映射
├── postmaster.pid # 运行中的实例信息(PID、端口、数据目录)
├── postmaster.opts # 上次启动时用的命令行参数
├── base/ # 各数据库的数据文件,子目录名 = 数据库 OID
│ ├── 1/ # template1
│ ├── 4/ # template0
│ ├── 5/ # postgres
│ └── 16384/ # 用户创建的数据库
│ ├── 16385 # 某个表的 relfilenode 文件
│ ├── 16385_fsm # 该表的空闲空间映射
│ ├── 16385_vm # 该表的可见性映射
│ └── pg_filenode.map # 系统目录的 relfilenode 映射表
├── global/ # 跨数据库共享的系统目录(pg_database、pg_authid 等)
├── pg_wal/ # 预写日志(WAL)段文件
│ ├── 000000010000000000000001
│ ├── archive_status/ # 归档状态标记
│ └── summaries/ # PostgreSQL 17+:WAL 摘要(增量备份用)
├── pg_xact/ # 事务提交日志(CLOG),记录每个事务的最终状态
├── pg_multixact/ # 多事务状态(行级锁等待、共享锁)
├── pg_subtrans/ # 子事务状态
├── pg_commit_ts/ # 提交时间戳(仅 track_commit_timestamp = on 时使用)
├── pg_tblspc/ # 表空间:指向 PGDATA 外部目录的符号链接
├── pg_replslot/ # 逻辑复制槽的持久化状态
├── pg_logical/ # 逻辑解码的快照与映射文件
├── pg_twophase/ # 两阶段提交的预备事务状态
├── pg_notify/ # LISTEN/NOTIFY 的持久化队列
├── pg_serial/ # 可串行化事务的冲突信息
├── pg_snapshots/ # 导出的快照(pg_export_snapshot())
├── pg_stat/ # 统计信息的持久化文件
└── pg_dynshmem/ # 动态共享内存的元数据版本差异:两个改过名的目录
- PostgreSQL 10 之前:
pg_wal叫pg_xlog,pg_xact叫pg_clog。老文章里的这两个名字指的就是现在的目录。 - PostgreSQL 15 起:统计收集器进程(stats collector)被移除,统计信息改为存放在共享内存中并定期落盘,
pg_stat/目录的内容随之改变。 - PostgreSQL 17 起:新增
pg_wal/summaries/目录,用于支持增量备份(pg_basebackup --incremental)。
核心机制
数据目录的划分逻辑
四个主目录的分工非常清晰,理解它们就理解了一半:
| 目录 | 存放内容 | 是否可跨库访问 | 命名方式 |
|---|---|---|---|
base/ | 普通数据库的所有表、索引、TOAST 等 | 否,各自隔离在数据库子目录内 | 子目录名 = 数据库 OID,文件名 = relfilenode |
global/ | 集群级系统目录:pg_database、pg_authid、pg_tablespace 等 | 是,所有数据库共享 | 文件名 = relfilenode |
pg_wal/ | 预写日志段文件 | 是,全局唯一序列 | 24 位十六进制名,含时间线、逻辑段号 |
pg_xact/ | 事务提交状态(每个事务 2 bit) | 是,全局共享 | 4 位十六进制名,每个文件 256KB |
base/ 下的每个子目录就是一个数据库。这也解释了为什么 PostgreSQL 不能跨库查询——两个数据库的数据物理上锁在不同的目录里,没有统一的执行计划能同时扫描两边。
从表名到磁盘文件的完整链路
一条 SQL 里的表名,要经过四次映射才能变成磁盘上的字节:
t_user(表名)
↓ 查 pg_class(在 pg_namespace 中按 search_path 定位到具体 schema)
pg_class.oid = 16385 ← 关系 OID
↓ 取 relfilenode 字段
pg_class.relfilenode = 16385 ← 物理主文件名(绝大多数情况等于 oid)
↓ 拼接数据库目录(pg_database.oid)
base/16384/16385
↓ 加上分叉后缀
base/16384/16385 (main, 主数据)
base/16384/16385_fsm (空闲空间映射)
base/16384/16385_vm (可见性映射)
base/16384/16385_init (init fork,仅无日志表有)只需要一条内置函数就能拿到最终路径:
SELECT pg_relation_filepath('t_user'); pg_relation_filepath
----------------------
base/16384/16385
(1 row)为什么需要 relfilenode 而不是直接用 oid? 因为有些操作需要整体重建物理文件:TRUNCATE、VACUUM FULL、CLUSTER、REINDEX。如果文件名恒等于 OID,重建时就无法原子切换;引入一层可变的 relfilenode,新文件写好、元数据一提交、旧文件延迟删除——整个替换过程对并发事务是原子的。
段文件切分:为什么一张表会变多个文件
单个物理文件存在上限。PostgreSQL 约定每个文件最大 1GB,超出部分自动切分为 xxx.1、xxx.2……
base/16384/
├── 16385 # 第 1 个 1GB:0 ~ 1073741823 字节
├── 16385.1 # 第 2 个 1GB
├── 16385.2 # 第 3 个 1GB
├── 16385_fsm # 空闲空间映射(自身也可切分为 16385_fsm.1)
└── 16385_vm # 可见性映射这样设计有三个好处:绕开部分文件系统对单文件大小的限制(如老式 32 位系统、某些 NFS)、便于按需增长、pg_relation_size() 只需累加段文件即可得到表大小。
| 文件(分叉) | 无后缀(main) | _fsm | _vm | _init |
|---|---|---|---|---|
| 内容 | 表/索引的数据块本身 | 空闲空间映射(Free Space Map) | 可见性映射(Visibility Map) | 无日志表的初始状态 |
| 哪类对象有 | 所有有存储的对象 | 表、物化视图 | 表、物化视图 | 仅无日志表(UNLOGGED) |
| 是否可缺省 | 否 | 是(重建前可能为空) | 是(VACUUM 前为空) | 是(非无日志表没有) |
详细说明
base/ 与 global/:数据文件的家
base/ 下层是数据库,global/ 层直接就是文件。两者的文件名规则相同,都是 relfilenode。
-- 数据库 OID 与名称的对应关系,决定了 base/ 下的子目录名
SELECT oid, datname, pg_size_pretty(pg_database_size(datname)) AS size
FROM pg_database
ORDER BY oid; oid | datname | size
-------+-------------+---------
1 | template1 | 7696 kB
4 | template0 | 7613 kB
5 | postgres | 7833 kB
16384 | app_prod | 24 MB
(4 rows)# ── 用 OID 去磁盘上找对应的数据库目录 ──
ls -1 "$PGDATA/base" | sort -n | head -n 10
# ── 查看某个数据库目录里的数据文件(按大小排序,最大的就是最热的表) ──
ls -lhS "$PGDATA/base/16384" | head -n 15# base/16384 目录下的真实文件(节选)
-rw------- 1 postgres postgres 1.0G 16385 # 单表已达 1GB,即将切分
-rw------- 1 postgres postgres 1.0G 16385.1 # 第二个段文件
-rw------- 1 postgres postgres 256M 16386
-rw------- 1 postgres postgres 1.1M 16385_fsm
-rw------- 1 postgres postgres 136K 16385_vm
-rw------- 1 postgres postgres 8.0K pg_filenode.mapbase/ 和 global/ 下各有一个 pg_filenode.map 文件。它保存着关键系统目录的 relfilenode 映射——因为系统目录的 OID 是硬编码在源码里的(如 pg_class 的 OID 固定为 1259),但它们的物理文件名会变,需要这张映射表来兜底。这个文件被损坏时集群直接无法启动,绝不能手工编辑或替换。
-- 查看系统目录的 OID 与 relfilenode 是否一致
SELECT oid, relname, relfilenode, relkind
FROM pg_class
WHERE relname IN ('pg_class', 'pg_database', 'pg_attribute', 'pg_proc')
ORDER BY oid; oid | relname | relfilenode | relkind
-------+---------------+---------------+---------
1255 | pg_proc | 1255 | r
1259 | pg_class | 1259 | r
1249 | pg_attribute | 1249 | r
1262 | pg_database | 1262 | r
(4 rows)pg_database 为什么在 global/ 里
pg_database、pg_authid、pg_tablespace、pg_shdescription 这类目录需要被所有数据库看到(否则连不上其他库都不知道有哪些库),所以它们放在 global/ 而不是某个 base/<oid>/ 里。
relfilenode 会变的三种情况
这是排查磁盘问题时最容易踩的坑:pg_class.relfilenode 不等于 pg_class.oid 时,说明这张表被重建过。
-- 建表并记录初始 relfilenode
CREATE TABLE t_user (id int PRIMARY KEY, name text);
SELECT oid, relname, relfilenode FROM pg_class WHERE relname = 't_user'; oid | relname | relfilenode
-------+---------+-------------
16385 | t_user | 16385
(1 row)-- TRUNCATE 之后,OID 不变,但 relfilenode 变了(物理文件被整体替换)
TRUNCATE t_user;
SELECT oid, relname, relfilenode FROM pg_class WHERE relname = 't_user'; oid | relname | relfilenode
-------+---------+-------------
16385 | t_user | 16402 -- 变了!旧文件 16385 已被删除
(1 row)| 操作 | relfilenode 是否改变 | 原因 |
|---|---|---|
TRUNCATE | 是 | 直接丢弃旧文件,建一个新的空文件,避免逐页清理 |
VACUUM FULL | 是 | 重写整表以彻底回收空间,完成后替换文件 |
CLUSTER | 是 | 按索引顺序重写整表 |
REINDEX(索引) | 是 | 索引重建为新文件 |
ALTER TABLE ... SET TABLESPACE | 是 | 物理文件搬到另一个表空间 |
INSERT / UPDATE / DELETE | 否 | 原地修改页面 |
普通 VACUUM | 否 | 只在页内整理,不重建文件 |
relfilenode = 0 的情形
以下对象没有物理文件,relfilenode 为 0,pg_relation_filepath() 返回 NULL:
- 视图(
relkind = 'v')、外部表('f') - 分区表的父表(
'p',数据都在各个分区子表里) - 序列在部分版本中的占位形态
排查「磁盘上没有这个文件」时,先确认对象类型,别急着怀疑数据损坏。
pg_wal/、pg_xact/ 与其他状态目录
这四类目录决定数据库能否从崩溃中恢复,是备份策略的核心对象。
(1)pg_wal/:预写日志
-- 当前正在写的 WAL 文件与位置
SELECT pg_current_wal_lsn() AS cur_lsn,
pg_walfile_name(pg_current_wal_lsn()) AS cur_file,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0')) AS written; cur_lsn | cur_file | written
----------+------------------------------+---------
0/1A2B3C8 | 000000010000000000000001 | 26 MB
(1 row)# ── WAL 段文件命名:时间线(8) + 逻辑段号(8) + 段内序号(8),共 24 位十六进制 ──
# 000000010000000000000001
# | | └── 1 亿分之一…… 实际是段内编号
# | └── 逻辑段号(logical log id)
# └── 时间线(timeline),主库通常为 00000001
ls -lh "$PGDATA/pg_wal" | head -n 10-rw------- 1 postgres postgres 16M 000000010000000000000001
-rw------- 1 postgres postgres 16M 000000010000000000000002
-rw------- 1 postgres postgres 16M 000000010000000000000003
drwx------ 2 postgres postgres 30 archive_status # 每个待归档段对应一个 .ready/.done 标记WAL 段文件默认 16MB(编译期 --with-wal-segsize 可改,初始化后不可变),文件名中的时间线(Timeline)用于区分主库与从库的 WAL 分支——做过主备切换后会看到 00000002、00000003 等。
(2)pg_xact/:事务提交日志(CLOG)
每个事务只需 2 bit 表示四种状态:进行中、已提交、已中止、子事务已提交。按此密度,一个 256KB 的文件可覆盖约 100 万个事务。
# ── 4 位十六进制文件名,每个文件固定 256KB ──
ls -lh "$PGDATA/pg_xact" | tail -n 5-rw------- 1 postgres postgres 256K 0000 # 覆盖 XID 0 起的一段事务
-rw------- 1 postgres postgres 256K 0001pg_xact 的空间占用会随着事务量增长,但不能手工清理——只有 VACUUM 完成冻结(freeze)后,老的事务号才可以被回收。这也是「XID 环绕」风险的物理根源:如果一直不 VACUUM,pg_xact/ 会持续膨胀,最终逼近 20 亿事务的上限而强制停机。
(3)其余状态目录
| 目录 | 用途 | 空间特征 |
|---|---|---|
pg_multixact/ | 多事务 ID,行级共享锁同时被多事务持有时使用 | 含 members/ 与 offsets/ 两个子目录,异常增长往往意味着长事务持有大量共享锁 |
pg_subtrans/ | 子事务(SAVEPOINT)的父事务映射 | 每个文件 256KB |
pg_commit_ts/ | 事务提交时间戳,需 track_commit_timestamp = on(需重启) | 默认不使用 |
pg_replslot/ | 逻辑/物理复制槽状态,决定 WAL 能否被回收 | 槽不推进即为 WAL 堆积的根因 |
pg_twophase/ | 两阶段提交的预备事务状态文件 | 每个预备事务一个文件,文件名为 GID |
pg_logical/ | 逻辑解码的快照与系统目录映射 | snapshots/、mappings/ |
pg_notify/ | LISTEN/NOTIFY 的持久化队列,仅在崩溃恢复时使用 | 通常很小 |
pg_serial/ | 可串行化隔离级别的冲突信息 | 通常很小 |
pg_snapshots/ | pg_export_snapshot() 导出的快照 | 用完即删 |
pg_stat/ | 统计信息持久化文件,重启后用于恢复累计计数 | 很小 |
pg_dynshmem/ | 动态共享内存的元数据 | 很小 |
-- 逻辑复制槽:WAL 无法回收的最常见原因
SELECT slot_name, plugin, active, wal_status,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots; slot_name | plugin | active | wal_status | retained_wal
-----------+----------+--------+------------+--------------
slot_a | pgoutput | t | reserved | 2.1 GB
slot_stale| pgoutput | f | extended | 48 GB -- 长期未消费,WAL 堆积
(2 rows)实战示例
示例 1:把一张表完整映射到磁盘文件
目标:从 SQL 一路追到 ls 看到的文件,验证整条链路。
-- 1. 建一张有 TOAST 潜力的表并插入一些数据
CREATE TABLE t_disk_demo (
id bigserial PRIMARY KEY,
note text
);
INSERT INTO t_disk_demo (note)
SELECT repeat('pg-disk-', 300) FROM generate_series(1, 2000);
-- 2. 查询逻辑信息与物理路径
SELECT c.oid,
c.relfilenode,
pg_relation_filepath(c.oid) AS filepath,
pg_size_pretty(pg_relation_size(c.oid)) AS main_size,
pg_size_pretty(pg_relation_size(c.oid, 'fsm')) AS fsm_size,
pg_size_pretty(pg_relation_size(c.oid, 'vm')) AS vm_size,
pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size
FROM pg_class c
WHERE c.relname = 't_disk_demo'; oid | relfilenode | filepath | main_size | fsm_size | vm_size | total_size
-------+-------------+-------------------+-----------+----------+---------+-----------
16410 | 16410 | base/16384/16410 | 440 kB | 24 kB | 8192 bytes | 1.8 MB
(1 row)# 3. 到磁盘上核对(会看到 main/fsm/vm 三个文件,以及被 TOAST 拆出去的大字段)
ls -lh "$PGDATA/base/16384/16410"*-rw------- 1 postgres postgres 440K 16410
-rw------- 1 postgres postgres 24K 16410_fsm
-rw------- 1 postgres postgres 8.0K 16410_vmtotal_size 明显大于 main_size,差额来自 TOAST 表——note 字段被压缩并搬到了独立的 TOAST 表里,它有自己的 relfilenode 与自己的一套磁盘文件。
-- 4. 找到这张表对应的 TOAST 表,它也是普通的 pg_class 记录
SELECT c.relname AS toast_table,
pg_relation_filepath(c.oid) AS toast_filepath,
pg_size_pretty(pg_relation_size(c.oid)) AS toast_size
FROM pg_class t
JOIN pg_class c ON c.oid = t.reltoastrelid
WHERE t.relname = 't_disk_demo'; toast_table | toast_filepath | toast_size
--------------------+-----------------------+------------
pg_toast_16410 | base/16384/16411 | 1344 kB
pg_toast_16410_idx | base/16384/16412 |
(2 rows)示例 2:观察段文件切分
目标:制造一张超过 1GB 的表,看磁盘上出现 .1、.2 段文件。
-- 写入约 1.2GB 数据(需保证盘上有足够空间,建议在测试库执行)
CREATE TABLE t_seg_demo (id int, payload text);
INSERT INTO t_seg_demo
SELECT g, repeat('x', 1000)
FROM generate_series(1, 1200000) AS g;
SELECT pg_size_pretty(pg_relation_size('t_seg_demo')) AS table_size,
pg_relation_filepath('t_seg_demo') AS filepath; table_size | filepath
------------+-----------------
1256 MB | base/16384/16420
(1 row)# ── 1GB 处切分:主文件 + .1 段文件 ──
ls -lh "$PGDATA/base/16384/16420"*-rw------- 1 postgres postgres 1.0G 16420 # 0 ~ 1073741823
-rw------- 1 postgres postgres 232M 16420.1 # 溢出的部分
-rw------- 1 postgres postgres 24K 16420_fsm
-rw------- 1 postgres postgres 8.0K 16420_vm-- 段文件数量也可由「文件大小 / 1GB」推算,但索引与 TOAST 不计入 pg_relation_size
SELECT (pg_relation_size('t_seg_demo') / 1024 / 1024 / 1024)::int + 1 AS approx_segments;示例 3:核对表空间的实际落盘位置
表空间是唯一能让数据文件离开 PGDATA 的机制,pg_tblspc/ 里存的只是符号链接。
-- 1. 查看表空间清单与真实路径
SELECT spcname, oid, pg_tablespace_location(oid) AS location FROM pg_tablespace; spcname | oid | location
--------------+-------+-------------------------
pg_default | 1663 | -- 空表示就在 PGDATA/base 下
pg_global | 1664 | -- 空表示就在 PGDATA/global 下
ts_fast | 16430 | /data/pg_tablespaces/ts_fast
(3 rows)-- 2. 把一张表挪进新表空间(会重写整表,relfilenode 改变)
ALTER TABLE t_seg_demo SET TABLESPACE ts_fast;
SELECT pg_relation_filepath('t_seg_demo') AS new_filepath, relfilenode
FROM pg_class WHERE relname = 't_seg_demo'; new_filepath | relfilenode
------------------------------------------------+-------------
pg_tblspc/16430/PG_18_202506071/16384/16435 | 16435
(1 row)# 3. pg_tblspc/16430 只是一个软链接;版本号子目录用于隔离不同大版本的物理格式
ls -l "$PGDATA/pg_tblspc/"lrwxrwxrwx 1 postgres postgres 30 pg_tblspc/16430 -> /data/pg_tablespaces/ts_fast注意路径中的 PG_18_202506071 是目录版本号(Catalog Version)目录:不同大版本共享同一个表空间物理目录时,各自的版本子目录互不干扰,避免降级/并存时的物理格式冲突。
示例 4:定位「磁盘上有文件但表里查不到」
这是运维中很常见的一类问题,通常有三个原因。
-- 原因 A:对象属于另一个数据库(base/<oid> 与库名对不上)
SELECT d.datname, c.relname, c.relfilenode
FROM pg_database d
JOIN pg_class c ON true -- 示意:实际需连到目标库中查询
WHERE false;
-- 原因 B:这是被删除但尚未清理的「孤儿文件」,通常是崩溃后遗留下的
-- PG 在重启时会扫描并尝试归并/删除它们,日志里会出现:
-- WARNING: could not remove file "base/16384/16410": ...
-- 或 LOG: removing orphaned temporary file ...# 原因 C:这是临时文件或未提交的段文件,属于正常现象
ls -1 "$PGDATA/base/16384/" | grep -E '^pgsql_tmp' | head排查顺序建议:先按文件名查 pg_class.relfilenode,没有结果再看日志中的 orphan 提示,最后才考虑数据损坏。pg_relation_filepath() 是这一步最可靠的工具。
注意事项
- PGDATA 里的文件不能手工增删改:
pg_control、pg_filenode.map、pg_wal/的任何手工干预都可能导致实例无法启动。 - PGDATA 本身不能是符号链接:
initdb会拒绝在软链接目录上初始化;但pg_wal、pg_tblspc/<oid>可以是软链接(这是把 WAL 与数据分离到不同磁盘的官方手段)。 relfilenode不等于oid时不要当成异常:TRUNCATE、VACUUM FULL、CLUSTER、REINDEX都会改变它,这是设计行为。pg_relation_size()只算 main 分叉:不包含_fsm、_vm、TOAST 与索引;要算全量请用pg_total_relation_size()。pg_relation_filepath()对无存储对象返回 NULL:视图、外部表、分区父表的relfilenode为 0,属正常现象。- 段文件大小 1GB 与 WAL 段 16MB 是两回事:前者是数据文件切分粒度(编译期
--with-segmentsize),后者是 WAL 的粒度(--with-wal-segsize),初始化后都不可改。 pg_xact/膨胀是 XID 环绕的前兆:它不会自动收缩,只有 VACUUM 冻结事务后老段才能回收。- 复制槽目录会锁住 WAL:
pg_replslot/中失效的槽会让pg_wal/无限增长,是磁盘打满的头号原因。 archive_status/中堆积.ready文件说明归档失败:此时pg_wal/无法回收,需立刻检查归档命令与pg_stat_archiver。- 不要用
cp -r或tar备份正在运行的 PGDATA:必须用pg_basebackup或文件系统快照,否则拿到的是撕裂的物理状态。
# 不推荐:手工删除「看起来没用」的 WAL 文件
# rm -f "$PGDATA/pg_wal/00000001000000000000000A" # 不推荐,会导致崩溃恢复失败-- 不推荐:把 relfilenode 当作稳定的对象标识符持久化到业务表里
-- SELECT relfilenode FROM pg_class WHERE ...; -- 不推荐,重建后即失效
-- 推荐:业务系统只应保存 oid 或直接保存表名,物理文件路径属于内部实现最佳实践
pg_wal放到独立磁盘或独立卷:WAL 是顺序写且对延迟敏感,与随机读写的数据文件分离能显著降低提交延迟。pg_tblspc按 IO 特性分层:热表放 SSD 表空间,归档类大表放机械盘表空间,用ALTER TABLE ... SET TABLESPACE在线迁移。- 用
pg_relation_filepath()定位问题表:磁盘告警时,按文件大小排序后反查pg_class.relfilenode,能快速找出膨胀的表与索引。 - 定期统计
pg_class与磁盘实际占用的差异:差异过大通常意味着存在大量已删除对象残留或未回收的 TOAST 文件。 - 监控三个目录的增长趋势:
pg_wal/(归档/复制槽是否正常)、pg_xact/(VACUUM 是否跟上)、pg_multixact/(长事务是否过多)。 - 备份必须覆盖整个 PGDATA 加上表空间目录:
pg_basebackup会自动处理,手工备份方案则必须额外纳入pg_tblspc指向的外部路径。 data_checksums在initdb时就要决定:开启后可通过pg_checksums检测静默损坏,事后开启需要停机重写全部数据文件。- 文件系统选择 XFS 或 ext4:避免使用对 fsync 语义做手脚的文件系统;不要在 NFS 上跑 PGDATA(可靠性无法保证)。
- 给 PGDATA 留足 inode:一张大表会拆成多个段文件,宽表 + 大量索引会快速消耗 inode。
- 升级大版本时不要复制
base/目录:必须走pg_upgrade或逻辑复制,物理格式不跨大版本兼容。
下一节
继续学习:查询执行流程