PostgreSQL 架构简介

PostgreSQL 是一个开源的对象关系型数据库管理系统,已经持续开发超过 30 年。对于后端开发者而言,理解 PostgreSQL 的底层工作机制有助于编写更快的查询、设计更好的 schema,并有效地排查性能问题。

其核心架构由若干协同工作的层次组成。在最上层,客户端应用程序通过网络 socket,使用 PostgreSQL 有线协议进行连接。连接首先由 postmaster 进程接收,它会为每个客户端连接派生(fork)一个新的后端进程。这种每连接一进程的模型提供了强大的隔离性,但也意味着在高并发应用中应当使用连接池(如 PgBouncer)。

查询到达后端进程后,会流经以下查询处理器:解析器(parser)检查语法,分析器/重写器(analyzer/rewriter)应用规则并验证权限,规划器(planner)生成最优执行计划,执行器(executor)针对存储层运行该计划。存储层采用堆文件结构来写入和读取行,并使用独立的索引文件(通常是 B-tree)来对索引列提供快速查找。

设计规范化的关系型 schema

关系型设计是一种组织数据表的实践,旨在最小化冗余并保护数据完整性。其基础概念是规范化(normalization),它是一组规则(范式),用以指导如何将数据拆分到相关的表中。

最常用的范式是第一范式(1NF)、第二范式(2NF)和第三范式(3NF)。第一范式要求每个字段只包含原子值——单个单元格内不允许出现数组或逗号分隔的列表。第二范式在 1NF 的基础上进一步要求所有非键字段都依赖于完整的主键,这在复合键的场景下尤其重要。第三范式则更进一步:任何非键字段都不应依赖于其他非键字段。

考虑一个实际场景:你需要存储博客文章及其作者和标签。一种未规范化的做法是将所有内容放在一张表中,这会导致更新异常——如果某个作者修改了他们的邮箱,你就必须更新他们所写的每一行。而规范化的设计则会通过外键将其拆分为多张独立的表。

sql
CREATE TABLE authors (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
body TEXT,
author_id INTEGER NOT NULL REFERENCES authors(id),
published_at TIMESTAMPTZ
);
CREATE TABLE tags (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL
);
CREATE TABLE post_tags (
post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_id)
);

post_tags 表是一个连接表,用于解析文章与标签之间的多对多关系。请注意,SERIAL 提供了自增整数,REFERENCES 定义外键约束,而 ON DELETE CASCADE 则会在父记录被删除时自动删除对应的连接表记录。

数据类型与约束

PostgreSQL 提供了远超基础 INTEGER 和 VARCHAR 的丰富类型系统。对于后端开发者而言,一些特别实用的类型包括:用于带时区感知的时间戳的 TIMESTAMPTZ、用于存储支持索引的半结构化数据的 JSONB、用于全局唯一标识符的 UUID,以及用于真/假标志的 BOOLEAN。

约束在数据库层面强制保证数据完整性,而不是仅依赖应用代码。常见的约束包括 NOT NULL、UNIQUE、CHECK、FOREIGN KEY 和 PRIMARY KEY。使用约束意味着即使存在缺陷的应用程序尝试插入无效值,你的数据也能保持有效。

sql
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(200) NOT NULL,
price NUMERIC(10,2) CHECK (price >= 0),
stock INTEGER DEFAULT 0 CHECK (stock >= 0),
metadata JSONB DEFAULT '{}'
);

NUMERIC(10,2) 存储精确的小数值,非常适合用于货币金额,这种场景下浮点误差是不可接受的。CHECK 约束确保价格和库存永远不会是负数。metadata 上的 DEFAULT 子句意味着新行会自动获得一个空的 JSON 对象,而不是 NULL。

流程图展示了从客户端连接开始,经过解析器、分析器、规划器、执行器,最终到达存储层的查询处理管道

常见设计误区

一个常见的错误是使用 VARCHAR 时不指定长度限制,这可能导致行大小意外地变得非常庞大。另一个错误是到处都使用 TEXT,而不考虑该列是否真的需要无限长度——通过约束明确表达需求可以更早地发现 bug。开发者也经常忘记为外键列添加索引,这会导致 JOIN 操作和大表上的级联删除变慢。

过度规范化则是相反的陷阱:将数据拆分到过多的小表中会导致查询需要过多的 JOIN。对于高读取负载的工作负载,策略性的反规范化或物化视图可能是正确的权衡。目标是找到适合你特定访问模式的平衡点。

总结

PostgreSQL 采用多进程架构,配合其解析器-规划器-执行器管道,为你的数据库提供了可预测的性能特征。通过 1NF、2NF 和 3NF 进行规范化关系设计,可以减少冗余并防止更新异常,而外键和中间表则用于处理复杂的关系。PostgreSQL 丰富的类型系统和约束支持,让你能够在 schema 层面强制保证数据完整性,在错误影响到应用代码之前就将其捕获。在下一课中,我们将从 schema 设计转向针对这些结构编写高效的查询。

课程检查点

1. 哪个 PostgreSQL 进程负责接受新的客户端连接,并为每个连接派生一个后端进程?

2. 第三范式(3NF)在 1NF 和 2NF 之上还有哪些要求?

3. 在博客 schema 的示例中,为什么使用 post_tags 关联表,而不是在 posts 表中设置一个 tags 列?

4. 在需要精确精度的场景下,存储货币金额最合适的 PostgreSQL 数据类型是哪种?

5. 当外键约束上定义了 ON DELETE CASCADE 时会发生什么?

6. SQL 查询在 PostgreSQL 查询处理器中会经过哪些阶段?

7. 本课程警告的一个常见 schema 设计错误是什么?