上一篇我们拆解了 PostgreSQL 的进程与内存架构。这一篇,我们把视角从"进程内部"拉到"对象层级"。实例、集群、数据库、模式、表空间------这五个词在日常交流中经常被混用,但在 PostgreSQL 的语境里,它们指向的是不同层次、甚至不同性质 的东西。理清它们的关系,你才能理解一个实例下多个数据库如何共存、表空间到底在调什么、以及哪些隔离是逻辑的、哪些隔离才是物理的。
一、五个词,先对号入座
在 PostgreSQL 的官方文档中,层级关系写得非常明确:
cluster → database → schema → table(或其他对象)
但"cluster"这个词在这里需要特别小心:PostgreSQL 所说的"集群",通常指的是一个由 initdb 初始化的数据目录加上运行在其上的服务器进程,也就是一个实例,而不是多台机器组成的分布式集群。很多中文材料直接把 cluster 翻译为"集群",容易和"数据库集群(多节点)"混淆,阅读时需要结合上下文判断。
| 概念 | 本质 | 层级 | 关键特征 |
|---|---|---|---|
| 实例 / Cluster | 一个数据目录 + 一组进程 | 最外层 | 共享 WAL、配置、内存、后台进程 |
| 数据库 | 逻辑容器,带独立权限体系 | 实例之下 | 连接时必选一个数据库,一个连接不能跨库访问 |
| 模式(Schema) | 数据库内的命名空间 | 数据库之下 | 不同 schema 下可以有同名表;默认 public |
| 表空间 | 物理存储位置 | 跨数据库、跨实例 | 一个表空间可以存放多个数据库的对象;一个数据库的对象可以分布在多个表空间 |
一个需要先记住的关键区分:数据库和模式是逻辑概念,表空间是物理概念,两者是"正交"的------它们回答的是不同的问题。数据库回答"数据属于谁、按什么权限组织",表空间回答"数据存在哪块磁盘上"。
二、一个实例下多个数据库:隔离了什么,共享了什么
这是本篇的核心问题之一。
从用户视角 看,多个数据库之间是隔离 的:你连接时必须指定一个数据库,一个连接不能同时访问两个数据库 ;每个数据库有自己的系统目录、权限和对象空间。如果两个项目或两组用户基本互不关心 ,官方建议把它们放进不同的数据库,并分别配置授权和访问控制。
但从管理员视角 看,它们又紧密绑定在一起。它们共享的是同一套底层资源:
- 同一个 WAL 流 :这是最关键的共享点。所有数据库的变更写入同一个 WAL,备份和恢复的粒度是集群级的,而不是数据库级的。你不能对一个实例中的某个数据库单独做 PITR,也不能单独回滚它。
- 同一组内存和进程 :
shared_buffers、wal_buffers、checkpointer、background writer、walwriter 都由所有数据库共享。 - 同一套配置 :
postgresql.conf中的参数对所有数据库生效(少数参数可以在数据库级别用ALTER DATABASE ... SET覆盖,但核心的资源参数是全局的)。 - 同一个数据目录 :
base/下按数据库 OID 分目录存放,但 WAL、事务状态、全局系统表都是共享的。
所以,"多数据库"提供的是逻辑隔离和权限隔离,而不是资源隔离或故障隔离。一个数据库里的长事务、锁竞争、大量写入产生的 WAL,都会对整个实例中的其他数据库产生影响。这也是官方提醒"多数据库的收益需要与风险和限制一起权衡"的原因。
什么时候用多个数据库,什么时候用多个 schema?
官方给出的判断标准很简洁:
- 互不关心、需要强隔离 → 不同数据库,分别授权;
- 互相关联、需要互相访问对方对象 → 同一个数据库,不同 schema,利用命名空间隔离和权限控制实现模块化。
用 schema 的好处是:可以在同一个连接中直接跨 schema 引用对象,事务边界统一,JOIN 和子查询不受阻碍 。用数据库的好处是:连接级隔离更彻底,权限边界更清晰 。但后者的代价是,你失去了跨库查询的能力,只能依赖 postgres_fdw 或 dblink 做代理访问,而且备份恢复仍然是全实例粒度。
三、表空间:唯一能改变"数据放在哪"的工具
如果说数据库和模式是在逻辑层面做划分,表空间就是 PostgreSQL 中唯一能直接控制物理存储位置的机制。
表空间的物理映射:软链接
当你执行 CREATE TABLESPACE fast_ssd LOCATION '/mnt/nvme/pg_fast' 时,PostgreSQL 会在数据目录的 pg_tblspc/ 下创建一个以表空间 OID 命名的符号链接,指向你指定的外部目录。
$PGDATA/pg_tblspc/16385 → /mnt/nvme/pg_fast
在这个外部目录内部,对象仍然按"数据库 OID / relfilenode"的方式组织。这意味着:一个表空间可以同时存放多个数据库的表和索引,一个数据库的对象也可以分散在多个表空间中。表空间不隶属于某个数据库,它是实例级别的物理容器。
这个设计带来一个重要的运维含义:备份时必须同时备份 pg_tblspc/ 中的软链接和软链接指向的实际目录 。只备份 $PGDATA 会丢失表空间中的数据,因为真正存放表文件的目录在 PGDATA 之外。
表空间如何用于性能隔离
表空间最实用的价值,是把 I/O 特征不同的数据放到不同的存储设备上。
一个典型的做法是热表隔离:把高频访问的小表放到 NVMe SSD 对应的表空间,把低频访问的大表或归档数据放到大容量 HDD 对应的表空间。索引和表也可以分离------把重度使用的索引放在最快的设备上,减少随机读延迟。
但仅仅把数据放到不同磁盘上还不够。PostgreSQL 的查询优化器默认假设所有表空间的 I/O 成本是一致的 。如果 SSD 表空间和 HDD 表空间共用同一套 seq_page_cost 和 random_page_cost,优化器可能会做出错误的成本估算。
从 PostgreSQL 的某个版本开始,表空间可以单独设置 I/O 成本参数:
sql
ALTER TABLESPACE fast_ssd SET (
random_page_cost = 1.1,
seq_page_cost = 1.0,
effective_io_concurrency = 200
);
这组参数会覆盖该表空间中表的全局默认成本估算,让优化器知道"这里的随机读并不昂贵"。对于位于不同存储介质的表空间,这是让优化器做出正确决策的必要步骤。
表空间的边界:它能做什么,不能做什么
表空间能做的:物理位置隔离、按设备设置 I/O 成本、把表/索引/物化视图分别放到不同卷上。
表空间不能做的:它不提供 CPU、内存或锁层面的隔离,也不改变"所有数据库共享同一个 WAL"这一事实 。一个表空间上的大量写入,仍然会产生全实例共享的 WAL 压力。表空间是存储层 的调优工具,不是实例资源的隔离工具。
四、连接管理:每个连接绑定一个数据库
PostgreSQL 的连接模型对这个层级结构有一个直接体现:客户端在连接时必须指定一个数据库名,连接建立后,这个会话就绑定在那个数据库上,不能中途"切换"到另一个数据库 。你可以对同一个数据库开多个连接,也可以对不同数据库分别开连接,但单个连接无法跨库 。跨库访问只能通过 postgres_fdw 或 dblink 这类外部数据包装器,让一个数据库中的对象"代理"另一个数据库中的对象。
这个约束和"process-per-connection"的进程模型是配套的:每个 backend 进程在 fork 时就已经确定了它所服务的数据库,私有内存和缓存都围绕那个数据库工作。这也是为什么"多数据库"在 PostgreSQL 中是一个连接级的划分,而不是一个可以在会话内自由穿越的逻辑边界。
五、总结
| 问题 | 结论 |
|---|---|
| 实例/集群是什么? | 一个数据目录 + 一组共享 WAL、内存、配置的进程 |
| 多数据库隔离了什么? | 逻辑与权限,不隔离 WAL、内存、故障域 |
| 多数据库共享什么? | 同一个 WAL(备份恢复全实例粒度)、shared_buffers、后台进程、配置文件 |
| 数据库 vs 模式怎么选? | 互不关心 → 不同数据库;互相关联 → 同库不同 schema |
| 表空间的本质? | 物理存储位置,跨数据库、跨实例,与逻辑数据库正交 |
| 表空间如何调优? | 热表/索引分散到不同设备,并单独设置 I/O 成本参数让优化器感知差异 |
| 表空间的边界? | 只做存储层调优,不解决 WAL 共享和资源竞争 |
一句话:PostgreSQL 的多数据库给你的是"逻辑上的独立王国",但它们仍然住在同一栋房子里,共享同一个水电总闸(WAL)和同一套承重结构。表空间是你唯一可以改变"房间朝向哪块地皮"的工具,但它改变不了"所有房间共用同一个总闸"这件事。
下一篇,我们进入数据库文件系统与数据页结构:8KB 的页里到底装了什么?TOAST 如何让大字段不拖垮主表?
思考题:
你的 PostgreSQL 实例目前是一个数据库承载所有业务,还是按业务拆成了多个数据库?如果拆了,你有没有遇到过"备份只能全实例做、一个库出问题影响所有库"的困扰?另外,你有没有用过表空间做冷热分离------实际效果符合预期吗?欢迎在评论区聊聊。
