PostgreSQL 到底是什么?
如果你已经用过 MySQL,那么可以先记住一句话:PostgreSQL 是一款开源、强调标准与正确性的对象关系型数据库(Object-Relational Database)。 它既有关系型数据库的表、行、SQL 和事务,又在类型系统、扩展能力和复杂查询上走得更远。
它常被称作“世界上最先进的开源数据库”,这并不是自夸——它对 SQL 标准的支持相当完整,事务严格遵循 ACID,还内置了 JSONB、数组、范围、地理坐标等丰富类型,以及一个能把内核能力无限延伸的扩展机制。
一句话概括它的价值:
应用 → 标准 SQL → PostgreSQL(事务 + 索引 + 优化器 + 类型系统 + 扩展)→ 可靠落盘它采用类 BSD 的 PostgreSQL 许可证,完全免费且可商用;当前最新主版本是 18,本文的概念在 12 及以上版本都适用。
什么时候该考虑 PostgreSQL?
它并不是万能钥匙,但在这些场景里往往是很稳的选择:
- 业务对数据正确性和事务要求高,比如账务、订单、库存;
- 需要处理半结构化数据,希望在一张表里同时用好 JSON 和关系模型;
- 有复杂查询:多表连接、窗口函数、CTE 递归、聚合分析;
- 未来可能需要地理信息(PostGIS)、向量检索(pgvector)或时序(TimescaleDB),希望一个内核搞定。
反过来,如果只是缓存 KV、或者极端追求写入吞吐而能容忍弱一致,那么 Redis、专用列存或时序库可能更合适。选型永远是“看场景”,而不是“看谁名气大”。
它的内部长什么样?——进程与内存架构
想真正“了解” PostgreSQL,绕不开它的运行模型。和很多数据库不同,PostgreSQL 采用的是多进程架构:每建立一个连接,主进程就为它派生一个独立的后端进程来处理 SQL。
这张图里有几个角色值得记住:
- Postmaster(主进程):监听端口(默认 5432),负责接受连接并派生子进程,是整个服务的“门房”与“调度者”。
- Backend 进程:每个客户端连接对应一个,负责执行该连接发来的所有 SQL。它们彼此独立,互不阻塞。
- 共享内存:其中最重要的是
shared_buffers(缓存表和索引页,减少磁盘 IO)和WAL Buffers(暂存预写日志)。所有后端进程共享这块内存,这是它们协作的基础。 - 私有内存
work_mem:每个进程在排序、哈希、聚合时会用到自己的这块内存。 - 后台辅助进程:无需客户端触发就自动运行,包括 Background Writer、WAL Writer、Checkpointer、Autovacuum、统计与日志进程。
理解这个模型有一个非常实际的好处:它解释了为什么高并发连接会吃内存。 每个连接一个进程、每个进程一份 work_mem,连接数暴涨时内存开销会线性上升。所以生产环境常常在应用和数据库之间放一个连接池(如 PgBouncer)来复用少量物理连接,而不是让几千个连接直接压到数据库上。
WAL:为什么断电也不丢数据
图里反复出现的 WAL(Write-Ahead Log,预写日志) 是 PostgreSQL 可靠性的核心。它的规则很简单:任何对数据的修改,必须先把“我要改什么”写进 WAL 日志并落盘,才允许真正去改数据文件。
这样一来,即使进程崩溃或者服务器断电,重启时 PostgreSQL 也能拿着 WAL 把没来得及写入数据文件的改动“重放”一遍,恢复到崩溃前的一致状态。WAL 同时也是主从复制的基础——从库本质上就是在不断接收并重放主库的 WAL。
数据是怎么组织的?
PostgreSQL 的数据模型和你熟悉的关系型数据库一致,但层级和一些术语值得先理清:
| 概念 | 说明 |
|---|---|
| Cluster | 一个 PostgreSQL 实例,可包含多个数据库,共用一套进程 |
| Database | 数据库,相互隔离,连接时需指定具体连接哪个 |
| Schema | 数据库内的命名空间,用于分组表;默认是 public |
| Table | 表,数据的基本容器 |
| Row / Tuple | 行(元组),一条记录 |
| Column | 列,有明确的数据类型 |
这里有一个容易踩的点:PostgreSQL 里的 schema 不是“表结构”的意思,而是一个命名空间(类似 Java 的 package)。 一个数据库里可以有多个 schema,用来把表按业务分组,例如 sales.orders 和 hr.employees。默认所有表都建在 public 这个 schema 下。
丰富的数据类型是它的一大亮点
PostgreSQL 的类型系统远比“数字 + 字符串 + 日期”丰富,这也是很多人选择它的理由:
| 类别 | 常见类型 | 用途举例 |
|---|---|---|
| 数值 | integer bigint numeric real | ID、金额(用 numeric 防精度丢失) |
| 字符 | varchar(n) text char(n) | 名称、正文,text 无长度限制 |
| 时间 | date timestamp timestamptz interval | 尽量用带时区的 timestamptz |
| 布尔 | boolean | 开关、状态标志 |
| JSON | json jsonb | 半结构化数据,优先用 jsonb |
| 数组 | integer[] text[] | 标签、多值字段 |
| 其它 | uuid inet cidr range 及自定义枚举 | 主键、网络地址、区间 |
其中 jsonb 特别值得一提:它以二进制格式存储 JSON,支持索引和高效查询,让你可以在一张表里“既有严格的关系列,又有灵活的文档字段”。而金额、价格这类对精度敏感的字段一定要用 numeric,不要用 real/double 这类浮点类型,否则会出现 0.1 + 0.2 ≠ 0.3 的经典问题。
事务与 MVCC:读写为什么能互不打架
事务是数据库的灵魂。PostgreSQL 完整支持 ACID:
- A 原子性:事务里的操作要么全成功,要么全回滚;
- C 一致性:约束(主键、外键、检查约束)始终被满足;
- I 隔离性:并发事务之间相互独立,不会读到彼此的中间状态;
- D 持久性:一旦提交,数据不会因崩溃而丢失(靠的就是前面的 WAL)。
而让隔离性既正确又高性能的关键机制,叫 MVCC(Multi-Version Concurrency Control,多版本并发控制)。
它的核心思想只有一句:更新数据时不覆盖旧值,而是生成一个新版本的行;每个事务只能看到“对它可见”的那个版本。
配合上图理解这个过程:
- 一行数据最初是版本
v1(余额 100)。每个行版本都带着两个隐藏字段:xmin(哪个事务创建了它)和xmax(哪个事务作废了它)。 - 一个事务把余额改成 150,PostgreSQL 不会就地修改,而是新写一个版本
v2,同时把v1标记为“被 200 号事务作废”。 - 在更新之前就开始的读事务 A,看到的仍然是
v1,余额 100——它不会被写事务阻塞,也不会读到“改了一半”的脏数据。 - 在更新之后开始的读事务 B,看到的是
v2,余额 150。 - 当没有任何事务再需要
v1时,它就成了死元组(dead tuple),等待VACUUM回收空间。
MVCC 带来的最大好处是:读不阻塞写,写不阻塞读。 一个大查询在跑的时候,其它人照样可以更新数据,彼此看到的都是一致的快照。这也是 PostgreSQL 在高并发读写场景下表现稳定的重要原因。
它的代价是膨胀(bloat):频繁更新会累积大量死元组,需要 VACUUM 定期清理。好在 PostgreSQL 内置了 Autovacuum 后台进程自动做这件事,一般不需要手工干预,但了解它的存在能帮你排查“表越来越大、查询越来越慢”的问题。
隔离级别
PostgreSQL 支持标准的事务隔离级别,默认是 Read Committed:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
| Read Committed | 否 | 可能 | 可能 | 默认级别,每条语句看最新已提交快照 |
| Repeatable Read | 否 | 否 | 否* | 整个事务用同一个快照 |
| Serializable | 否 | 否 | 否 | 最严格,效果等价于串行执行 |
值得一提的是,PostgreSQL 的 Repeatable Read 已经能防住绝大多数幻读,而 Serializable 用的是 SSI(可串行化快照隔离)算法,在保证正确的同时仍尽量并发,只在检测到真正的冲突时才让某个事务回滚重试。
让查询变快:索引与优化器
当表里有百万、千万行时,能不能用好索引,直接决定查询是几毫秒还是几十秒。
PostgreSQL 的索引类型
和只有 B-tree 唱主角的数据库不同,PostgreSQL 提供了一整套索引类型,各有擅长:
| 索引类型 | 适合的场景 | 典型例子 |
|---|---|---|
| B-tree | 等值和范围查询,默认类型 | WHERE age > 18、排序、主键 |
| Hash | 只做等值查询 | WHERE token = '...' |
| GIN | 一个字段包含多个值的“包含”查询 | JSONB、数组、全文检索 |
| GiST | 几何、范围、最近邻等 | 地理坐标、范围重叠 |
| BRIN | 超大表中物理有序的列,索引体积极小 | 按时间递增写入的日志表 |
绝大多数情况下你用的都是 B-tree,它同时支持等值、范围和排序。但当你要在 jsonb 字段里查“包含某个 key”,或在数组里查“包含某个元素”时,GIN 才是正确答案;而对于按时间只增不减的海量日志表,BRIN 用极小的体积就能大幅加速时间范围过滤。
优化器:一条 SQL 的内部旅程
写下一条 SQL 后,PostgreSQL 并不是“照字面执行”,而是要经过一条流水线,其中最关键的一步是查询优化器在多种可能的执行方式里挑出代价最低的一种。
四个阶段各司其职:
- 解析(Parse):检查语法是否正确,生成语法树;
- 分析与重写(Analyze & Rewrite):确认表和列真实存在,把视图、规则展开成底层查询;
- 规划(Plan):优化器出场,估算每种候选方案的“代价”,选出最优执行计划;
- 执行(Execute):执行器按计划扫描、连接、聚合,取出数据返回。
优化器凭什么做判断?主要靠三样东西:由 ANALYZE 收集的统计信息(数据分布、行数估算)、可用的索引(该走索引还是全表扫描),以及连接方式(嵌套循环、哈希连接还是归并连接)。
这也解释了两个常见现象:为什么“加了索引查询就快了”,以及为什么“数据大改之后要更新统计信息(ANALYZE),否则优化器可能选错计划”。
想看优化器最终选了什么、每一步花了多久,用 EXPLAIN 和 EXPLAIN ANALYZE:
-- 只看计划,不真正执行
EXPLAIN SELECT * FROM users WHERE age > 30;
-- 真正执行并给出每步实际耗时和行数,排查慢查询的第一工具
EXPLAIN ANALYZE SELECT * FROM users WHERE age > 30;如果输出里出现 Seq Scan(全表扫描)而你的表又很大,往往就是该考虑加索引的信号;出现 Index Scan 则说明走上了索引。
上手:安装与连接
说了这么多概念,来跑一遍最小闭环。
安装
# macOS(Homebrew)
brew install postgresql@18
brew services start postgresql@18
# Ubuntu / Debian
sudo apt update
sudo apt install postgresql
# Docker(最快、最干净,推荐用来学习)
docker run --name pg -e POSTGRES_PASSWORD=secret -p 5432:5432 -d postgres:18用 psql 连接
psql 是官方自带的命令行客户端,也是日常最顺手的工具:
# 连接:-h 主机 -p 端口 -U 用户 -d 数据库
psql -h localhost -p 5432 -U postgres -d postgres进入后,一批以反斜杠开头的元命令非常常用,值得记住:
\l 列出所有数据库
\c dbname 切换到某个数据库
\dt 列出当前库的所有表
\d tablename 查看某张表的结构
\du 列出所有用户/角色
\dn 列出所有 schema
\x 开关“扩展显示”,宽表看起来更清爽
\timing 开关每条 SQL 的耗时显示
\q 退出SQL 基础操作速览
下面用一个用户表把最常用的 SQL 串起来,都可以直接照着敲。
建库、建表
-- 创建数据库
CREATE DATABASE blog;
-- 创建表
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY, -- 自增主键
name VARCHAR(50) NOT NULL,
email VARCHAR(120) UNIQUE NOT NULL, -- 唯一约束
age INTEGER CHECK (age >= 0), -- 检查约束
profile JSONB, -- 半结构化字段
created_at TIMESTAMPTZ DEFAULT now() -- 带时区的时间,默认当前
);BIGSERIAL 会自动创建一个自增序列作为主键;TIMESTAMPTZ DEFAULT now() 让每条记录自动带上创建时间。
增删改查(CRUD)
-- 插入
INSERT INTO users (name, email, age, profile)
VALUES ('小韩', 'han@example.com', 25, '{"city": "上海", "tags": ["dev", "blog"]}');
-- 查询:条件、排序、分页
SELECT id, name, age
FROM users
WHERE age >= 18
ORDER BY created_at DESC
LIMIT 10 OFFSET 0;
-- 更新
UPDATE users SET age = 26 WHERE email = 'han@example.com';
-- 删除
DELETE FROM users WHERE age IS NULL;查询 JSONB 字段
这是 PostgreSQL 相较传统关系库的一个爽点:
-- ->> 取出文本值;-> 取出 JSON 对象
SELECT name, profile ->> 'city' AS city
FROM users
WHERE profile ->> 'city' = '上海';
-- @> 判断“包含”,配合 GIN 索引可以很快
SELECT name FROM users
WHERE profile @> '{"tags": ["dev"]}';建索引与显式事务
-- 给常用查询条件加 B-tree 索引
CREATE INDEX idx_users_age ON users (age);
-- 给 jsonb 字段加 GIN 索引,加速“包含”查询
CREATE INDEX idx_users_profile ON users USING GIN (profile);
-- 显式事务:转账场景,要么都成功要么都回滚
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; -- 出错时改用 ROLLBACK;入门阶段最容易踩的坑
- 金额用了浮点类型。 价格、余额一律用
numeric,别用real/double,否则精度会悄悄出错。 - 时间不带时区。 优先用
timestamptz,跨时区业务能省掉大量麻烦。 - 误以为
schema是表结构。 它是命名空间,默认public,别和“表定义”搞混。 - 大表频繁更新却不管 VACUUM。 死元组累积会让表膨胀、查询变慢,留意 Autovacuum 是否正常工作。
- 慢查询不看执行计划就乱加索引。 先
EXPLAIN ANALYZE看清瓶颈,再决定索引怎么建,避免加了一堆没用的索引反而拖慢写入。 - 高并发直连数据库不用连接池。 每个连接一个进程,连接数失控会耗尽内存,生产环境记得上 PgBouncer 这类连接池。
一张图记住 PostgreSQL
如果只带走五句话,那就是这些:
- 它是对象关系型数据库,在关系模型之上提供了丰富类型(JSONB、数组)和强大的扩展生态;
- 它是多进程架构,一个连接一个后端进程,靠共享内存协作、靠 WAL 保证不丢数据;
- MVCC 让读写互不阻塞,代价是需要 VACUUM 清理旧版本;
- 它有多种索引(B-tree/GIN/GiST/BRIN…)和一个基于代价的优化器,
EXPLAIN ANALYZE是调优的第一工具; - 事务严格遵循 ACID,默认隔离级别是 Read Committed,正确性是它最看重的东西。
打好这张基础地图之后,再往下深入复制与高可用、分区表、逻辑复制、性能调优和各类扩展,都会顺畅很多。