数据库备份最危险的误区,不是“没有备份”,而是看到磁盘上躺着一个 .sql 或 .dump 文件,就默认自己已经有了可恢复的备份。
真正出事的时候,你才会发现:角色没备份、扩展没装、Owner 对不上、目标库已经有对象、备份文件损坏、版本不兼容,甚至从来没有真正执行过一次恢复。
这篇文章不只讲两个命令,而是完整走一遍 PostgreSQL 最常见的一条迁移链路:
源数据库 →
pg_dump→ 备份文件 → 传输 → 新 PostgreSQL →pg_restore/psql→ 校验 → 切换业务
本文主要面向日常开发、服务器迁移、Docker 数据库迁移和中小规模生产环境。命令按 PostgreSQL 18 官方文档核对,但大多数内容同样适用于 PostgreSQL 14~18。
![]()
上图来自 Wikimedia Commons。左边是 Plain SQL +
psql,右边是归档格式 +pg_restore。这两个恢复路径一定要分清。
一、先分清:pg_dump 到底是不是“完整备份”
PostgreSQL 官方把备份方式大致分成三类:
- SQL Dump,也就是本文主要讲的
pg_dump/pg_dumpall; - 文件系统级备份;
- 基于 WAL 的连续归档和时间点恢复(PITR)。
pg_dump 是逻辑备份。
它读数据库里的对象和数据,然后把它们变成 SQL 脚本或 PostgreSQL 自己的归档格式。它的优势是非常适合:
- 数据库迁移;
- 跨机器迁移;
- 跨大版本升级;
- 测试环境复制;
- 单库、单 Schema、单表备份;
- 将备份恢复到不同名字的新库;
- 有选择地恢复部分对象。
而且 pg_dump 会生成一致性导出,正常情况下不会阻塞数据库里的普通读写业务。
但它不是万能的。
如果你的要求是“凌晨 03:17:42 误删了一批数据,我要恢复到 03:17:41”,那么每天跑一次 pg_dump 根本解决不了问题。那属于 Base Backup + WAL Archiving + PITR 的范围。
所以可以先记住一句:
pg_dump 很适合迁移和逻辑备份,但不要把它误当成完整的生产灾备体系。
二、开始前先确认工具版本
先看服务器版本:
psql -h 127.0.0.1 -U postgres -d postgres -c 'select version();'
再看本机客户端:
pg_dump --version
pg_restore --version
psql --version
跨大版本迁移时这里非常重要。
PostgreSQL 的 pg_dump 可以连接较旧版本的服务器,但不能使用旧版 pg_dump 去导出比自己更新的 PostgreSQL Server。
例如:
Server: PostgreSQL 18
pg_dump: PostgreSQL 16
这种组合不要用。
我的习惯是:
跨大版本迁移时优先使用目标版本附带的 PostgreSQL Client 工具去读取源库。
例如从 PostgreSQL 15 迁移到 18,可以在迁移机上安装 PostgreSQL 18 Client,然后用 PostgreSQL 18 的 pg_dump 连接 PostgreSQL 15。
另外,逻辑 Dump 通常适合向相同版本或更高版本恢复;向更旧的大版本恢复并不保证兼容。
三、不要把数据库密码直接写进脚本
很多教程喜欢这么写:
PGPASSWORD='123456' pg_dump ...
临时测试可以,但长期脚本我不推荐这么干。
Linux 下更适合使用:
~/.pgpass
格式是:
hostname:port:database:username:password
例如:
127.0.0.1:5432:appdb:postgres:your_password
然后:
chmod 600 ~/.pgpass
后续执行:
pg_dump -h 127.0.0.1 -U postgres -d appdb
就不需要一直交互输入密码。
Windows 对应文件通常是:
%APPDATA%\postgresql\pgpass.conf
生产环境里的备份脚本也应该交给专门的备份账号执行,而不是到处塞超级用户密码。
四、第一种:Plain SQL,最直观的备份方式
最简单的命令:
pg_dump \
-h 127.0.0.1 \
-p 5432 \
-U postgres \
-d appdb \
> appdb.sql
生成的 appdb.sql 就是一份文本 SQL。
你甚至可以直接打开:
less appdb.sql
看到里面的:
CREATE TABLE ...;
ALTER TABLE ...;
COPY ... FROM stdin;
CREATE INDEX ...;
恢复 Plain SQL
先创建一个空数据库:
createdb \
-h 127.0.0.1 \
-U postgres \
appdb_restore
然后用 psql 恢复:
psql \
-X \
--set ON_ERROR_STOP=on \
-h 127.0.0.1 \
-U postgres \
-d appdb_restore \
-f appdb.sql
这里我特意加了两个参数。
-X
让 psql 不加载用户自己的 psqlrc 配置,尽量保证恢复过程不被本机个性化配置干扰。
ON_ERROR_STOP=on
默认情况下,psql 执行脚本时遇到部分 SQL 错误可能会继续向下跑。
迁移数据库时这不是一个好习惯。
我更希望:
一旦发生真正的 SQL 错误,恢复立刻失败,而不是最后得到一个“不知道少了什么”的半残数据库。
所以建议:
psql -X --set ON_ERROR_STOP=on ...
Plain SQL 的优缺点
优点:
- 人类可直接阅读;
- 可以手动修改;
- 排查兼容问题方便;
- 只依赖
psql就能恢复。
缺点也很明显:
- 不能使用
pg_restore灵活选择对象; - 不适合复杂的大库恢复;
- 不方便并行恢复;
- 文件通常比较大。
所以对我来说,Plain SQL 更像是“小库、调试、需要人工查看 SQL”时的方案。
五、第二种:Custom Format,这是我最常用的方式
如果让我给绝大多数日常 PostgreSQL 迁移选一个默认方案,我一般选:
pg_dump -Fc
完整命令:
pg_dump \
-h 127.0.0.1 \
-p 5432 \
-U postgres \
-d appdb \
-Fc \
-f appdb.dump
-Fc 的意思是:
Format = Custom
这种文件不能直接用文本编辑器阅读,但它有几个非常重要的优势:
- 默认压缩;
- 支持
pg_restore; - 可以只恢复某个表、Schema 或某些对象;
- 可以改变恢复顺序;
- 支持并行恢复;
- 迁移时比一坨超大的
.sql更舒服。
看看备份里面到底有什么
pg_restore -l appdb.dump
它会显示归档的 Table of Contents,例如:
SCHEMA public
TABLE public users
TABLE DATA public users
SEQUENCE users_id_seq
INDEX users_pkey
CONSTRAINT ...
这一步其实非常适合做备份后的快速检查。
恢复 Custom Format
先创建目标库:
createdb -h 10.0.0.20 -U postgres appdb
然后:
pg_restore \
-h 10.0.0.20 \
-p 5432 \
-U postgres \
-d appdb \
--verbose \
--exit-on-error \
appdb.dump
这里的:
--exit-on-error
作用和前面 ON_ERROR_STOP 类似:恢复出错直接失败。
六、目标数据库里已经有东西怎么办?
这是迁移时最常碰到的问题之一。
如果目标数据库已经有旧表、旧函数和旧视图,直接恢复往往会得到大量:
already exists
multiple primary keys
relation already exists
如果你非常确定目标库里的内容应该被覆盖,可以:
pg_restore \
-h 10.0.0.20 \
-U postgres \
-d appdb \
--clean \
--if-exists \
--exit-on-error \
appdb.dump
其中:
--clean
会在恢复对象前尝试删除目标对象。
而:
--if-exists
可以减少目标对象本来就不存在时产生的一堆无意义错误。
不过如果这是正式迁移,我反而更喜欢另一种做法:
不要往旧数据库硬灌,而是创建一个全新的空库恢复,验证通过后再切连接。
例如:
appdb_old
appdb_migration_20260813
确认新库没问题后,再切应用的 DATABASE_URL。
这样出问题时回滚要简单得多。
七、Owner 和权限问题:迁移里最常见的坑之一
假设源服务器上有一个角色:
app_owner
但目标机器上根本没有这个角色。
恢复时很容易看到:
ERROR: role "app_owner" does not exist
如果你的目标是“把数据库完整搬到另一套环境,但不要求继承原服务器的角色体系”,最方便的是备份时:
pg_dump \
-h 10.0.0.10 \
-U postgres \
-d appdb \
-Fc \
--no-owner \
--no-acl \
-f appdb.dump
或者恢复时:
pg_restore \
--no-owner \
--no-acl \
-d appdb \
appdb.dump
两个参数分别解决:
--no-owner 不恢复原来的对象 Owner
--no-acl 不恢复原来的 GRANT / REVOKE 权限
这特别适合:
- 生产 → 测试;
- 自建服务器 → 云数据库;
- 一个团队环境 → 另一个团队环境;
- 原数据库角色结构已经不想保留的迁移。
但如果你是真的要“完整搬家”,角色就应该一起迁。
这就涉及 pg_dumpall。
八、pg_dump 不会替你备份所有角色
这是很多第一次做整库迁移的人会漏掉的地方。
pg_dump 只备份一个数据库里的对象。
数据库集群级别的全局对象,例如:
- Roles;
- 部分全局权限;
- Tablespaces;
并不属于某一个具体数据库。
要备份它们,需要:
pg_dumpall \
-h 10.0.0.10 \
-U postgres \
--globals-only \
> globals.sql
目标服务器恢复:
psql \
-X \
--set ON_ERROR_STOP=on \
-h 10.0.0.20 \
-U postgres \
-d postgres \
-f globals.sql
然后再恢复数据库本体:
pg_restore \
-h 10.0.0.20 \
-U postgres \
-d appdb \
appdb.dump
所以一次真正比较完整的逻辑迁移,通常是:
globals.sql
appdb.dump
两份东西。
注意:globals.sql 非常敏感
Role 的定义可能包含密码验证信息。
因此这类文件必须按敏感凭据处理:
chmod 600 globals.sql
更不要把它扔进 Git 仓库。
九、整台 PostgreSQL 有很多数据库怎么办?
如果实例里只有一个业务数据库,我更倾向于:
pg_dumpall --globals-only
+
每个数据库单独 pg_dump -Fc
因为这样恢复和排错更灵活。
当然 PostgreSQL 也提供:
pg_dumpall > cluster.sql
它会把整个 PostgreSQL Cluster 中的数据库和全局对象导出成一个 SQL 脚本。
恢复:
psql \
-X \
--set ON_ERROR_STOP=on \
-U postgres \
-d postgres \
-f cluster.sql
它很直接,但问题也很直接:
- 所有数据库塞进一个大 SQL;
- 选择性恢复不方便;
- 大实例恢复不够灵活。
因此实际迁移中,我往往还是分库备份。
例如:
pg_dumpall --globals-only > globals.sql
pg_dump -Fc app_a -f app_a.dump
pg_dump -Fc app_b -f app_b.dump
pg_dump -Fc analytics -f analytics.dump
目录一眼就能看懂:
backup/
├── globals.sql
├── app_a.dump
├── app_b.dump
└── analytics.dump
十、大数据库:目录格式 + 并行备份
如果数据库已经很大,一条 pg_dump -Fc 跑很久,可以考虑 Directory Format:
pg_dump \
-h 10.0.0.10 \
-U postgres \
-d appdb \
-Fd \
-j 4 \
-f appdb_dumpdir
这里:
-Fd Directory Format
-j 4 4 个并行 Worker
非常重要的一点是:
只有 Directory Format 支持 pg_dump 并行备份。
不要写成:
pg_dump -Fc -j 4 ...
然后期待 Custom Format 并行 Dump。
Directory Format 会生成一个目录:
appdb_dumpdir/
├── toc.dat
├── 3361.dat.gz
├── 3362.dat.gz
├── 3363.dat.gz
└── ...
恢复时同样可以并行:
pg_restore \
-h 10.0.0.20 \
-U postgres \
-d appdb \
-j 4 \
--exit-on-error \
appdb_dumpdir
Custom Format 虽然不能并行备份,但是可以并行恢复:
pg_restore -j 4 -d appdb appdb.dump
所以:
| 格式 | 并行备份 | 并行恢复 | 可读性 | 灵活性 |
|---|---|---|---|---|
| Plain SQL | 否 | 否 | 很高 | 一般 |
Custom -Fc |
否 | 是 | 低 | 很高 |
Directory -Fd |
是 | 是 | 低 | 很高 |
Tar -Ft |
否 | 有限制 | 低 | 一般 |
日常中小库,我通常选 -Fc。
真正的大库,才考虑 -Fd -j N。
-j 不是越大越好
并行 Dump 会增加数据库服务器负载,而且会建立多条数据库连接。
例如:
-j 8
不代表一定比:
-j 4
快一倍。
瓶颈可能在:
- 磁盘 I/O;
- 网络;
- CPU;
- 数据库连接数;
- 某几张特别大的表。
生产环境别一上来就:
-j 32
然后顺手把线上数据库压死。
十一、只备份一个表、一个 Schema
有时并不需要整库。
单表
pg_dump \
-h 127.0.0.1 \
-U postgres \
-d appdb \
-Fc \
-t public.users \
-f users.dump
恢复:
pg_restore \
-h 127.0.0.1 \
-U postgres \
-d appdb_restore \
users.dump
单个 Schema
pg_dump \
-h 127.0.0.1 \
-U postgres \
-d appdb \
-Fc \
-n business \
-f business.dump
只备份结构
pg_dump \
-U postgres \
-d appdb \
--schema-only \
> schema.sql
只备份数据
pg_dump \
-U postgres \
-d appdb \
--data-only \
> data.sql
这对开发环境复制、数据修复和上线前备份某张关键表都很好用。
不过要注意依赖关系。
只备份一个表,并不意味着 PostgreSQL 会自动帮你把它依赖的所有外部对象全部打包好。
十二、Docker 里的 PostgreSQL 怎么备份?
这是现在非常常见的场景。
假设容器叫:
postgres
数据库:
appdb
用户:
postgres
直接从容器导出 Custom Dump
docker exec postgres \
pg_dump \
-U postgres \
-d appdb \
-Fc \
> appdb.dump
注意这里我没有写:
docker exec -t
因为 Custom Dump 是二进制归档。
不要为了“终端看起来正常”给二进制输出乱分配 TTY。
备份出来后检查:
ls -lh appdb.dump
pg_restore -l appdb.dump | head
如果宿主机没有 pg_restore,也可以让容器自己检查:
docker exec -i postgres \
pg_restore -l \
< appdb.dump \
| head
恢复到另一个 PostgreSQL 容器
docker exec -i postgres-new \
pg_restore \
-U postgres \
-d appdb \
--exit-on-error \
< appdb.dump
Plain SQL 则是:
docker exec -i postgres-new \
psql \
-X \
--set ON_ERROR_STOP=on \
-U postgres \
-d appdb \
< appdb.sql
为什么不直接复制 PostgreSQL 的 data volume?
例如:
/var/lib/postgresql/data
直接复制 PGDATA 和逻辑备份根本不是一回事。
如果 PostgreSQL 仍在运行,你直接 cp -r 数据目录,完全可能拿到一个文件间状态不一致的副本。
物理备份有自己的规则。
如果你只是做容器迁移,我通常更愿意:
pg_dump -> 新容器 -> pg_restore
简单、透明,也更适合跨 PostgreSQL 大版本。
十三、完整实战:把一台服务器上的 PostgreSQL 搬到另一台
现在来走一遍真正的迁移。
假设:
源服务器:10.0.0.10
目标服务器:10.0.0.20
数据库:appdb
用户:postgres
第 1 步:确认版本
psql -h 10.0.0.10 -U postgres -d postgres -c 'select version();'
psql -h 10.0.0.20 -U postgres -d postgres -c 'select version();'
目标 PostgreSQL 大版本不能比你的恢复需求更老到无法兼容 Dump。
第 2 步:检查扩展
源库:
SELECT extname, extversion
FROM pg_extension
ORDER BY extname;
常见的:
pgcrypto
uuid-ossp
postgis
vector
目标服务器必须先具备对应扩展的软件文件。
pg_dump 可以带上:
CREATE EXTENSION ...
但它没能力凭空给目标 Linux 安装 PostGIS 或 pgvector 软件包。
这两个概念别混了。
第 3 步:备份全局角色
mkdir -p pg-migration
cd pg-migration
pg_dumpall \
-h 10.0.0.10 \
-U postgres \
--globals-only \
> globals.sql
第 4 步:备份数据库
pg_dump \
-h 10.0.0.10 \
-p 5432 \
-U postgres \
-d appdb \
-Fc \
--verbose \
-f appdb.dump
现在目录:
pg-migration/
├── globals.sql
└── appdb.dump
第 5 步:给备份计算 SHA256
Linux:
sha256sum globals.sql appdb.dump > SHA256SUMS
传输后再执行:
sha256sum -c SHA256SUMS
出现:
globals.sql: OK
appdb.dump: OK
至少说明文件在传输过程中没有悄悄损坏。
第 6 步:传到新服务器
scp globals.sql appdb.dump SHA256SUMS \
root@10.0.0.20:/opt/pg-migration/
大文件更推荐:
rsync -avP pg-migration/ \
root@10.0.0.20:/opt/pg-migration/
因为中断后更容易续传。
第 7 步:恢复角色
psql \
-X \
--set ON_ERROR_STOP=on \
-h 127.0.0.1 \
-U postgres \
-d postgres \
-f globals.sql
第 8 步:创建空数据库
createdb \
-h 127.0.0.1 \
-U postgres \
appdb_migrated
第 9 步:恢复
pg_restore \
-h 127.0.0.1 \
-U postgres \
-d appdb_migrated \
--verbose \
--exit-on-error \
-j 4 \
appdb.dump
第 10 步:ANALYZE
逻辑恢复后,我通常会补:
vacuumdb \
-h 127.0.0.1 \
-U postgres \
-d appdb_migrated \
--analyze-in-stages
或者至少:
ANALYZE;
让查询优化器尽快获得目标环境上的统计信息。
十四、恢复完不能只看“命令返回 0”
真正的恢复校验至少做几层。
1. 数据库对象数量
SELECT count(*)
FROM pg_tables
WHERE schemaname NOT IN ('pg_catalog', 'information_schema');
检查源库和目标库是否明显不一致。
2. 关键业务表行数
例如:
SELECT count(*) FROM users;
SELECT count(*) FROM orders;
SELECT count(*) FROM payments;
别把所有大表都 count(*) 一遍把磁盘扫爆。
优先选关键表。
3. 数据库大小
SELECT pg_size_pretty(pg_database_size(current_database()));
大小不是严格校验,但差出几个数量级肯定有问题。
4. 扩展
SELECT extname, extversion
FROM pg_extension
ORDER BY extname;
5. Sequence
迁移后最隐蔽的一类问题,是 Sequence 状态异常。
例如应用插入第一条新数据就出现:
duplicate key value violates unique constraint
因此一定要真正跑业务写入测试。
6. 应用级 Smoke Test
数据库迁移真正的验收不是:
pg_restore finished
而是:
- 登录正常;
- 查询正常;
- 新增数据正常;
- 更新正常;
- 删除正常;
- 后台任务正常;
- 权限正常;
- 定时任务正常。
数据库能连上和业务能运行,是两件事。
十五、最好先做一次“恢复演练”
这是我最建议养成的习惯。
假设每天凌晨备份:
/backup/postgresql/appdb-2026-08-13.dump
不要只检查:
ls -lh
更靠谱的做法是定期拿最新备份恢复到一个临时库:
createdb backup_verify
pg_restore \
--exit-on-error \
-d backup_verify \
appdb-2026-08-13.dump
恢复成功以后跑几条校验 SQL,再删掉:
dropdb backup_verify
因为:
一个从来没有成功恢复过的备份,只能叫“备份文件”,不能证明它真的能救命。
十六、一个简单的自动备份脚本
例如:
#!/usr/bin/env bash
set -Eeuo pipefail
BACKUP_DIR="/backup/postgresql"
DB_HOST="127.0.0.1"
DB_PORT="5432"
DB_USER="postgres"
DB_NAME="appdb"
DATE="$(date +%Y%m%d-%H%M%S)"
FILE="${BACKUP_DIR}/${DB_NAME}-${DATE}.dump"
mkdir -p "$BACKUP_DIR"
pg_dump \
-h "$DB_HOST" \
-p "$DB_PORT" \
-U "$DB_USER" \
-d "$DB_NAME" \
-Fc \
-f "$FILE"
pg_restore -l "$FILE" >/dev/null
sha256sum "$FILE" > "${FILE}.sha256"
find "$BACKUP_DIR" \
-type f \
-name "${DB_NAME}-*.dump" \
-mtime +14 \
-delete
find "$BACKUP_DIR" \
-type f \
-name "${DB_NAME}-*.dump.sha256" \
-mtime +14 \
-delete
echo "Backup completed: $FILE"
然后通过 systemd timer 或 Cron 调度。
不过请注意:
pg_restore -l
只能说明归档结构至少能被读取,不等于恢复演练。
真正的备份验证仍然应该定期还原数据库。
十七、几个特别常见的错误
错误 1:拿 pg_restore 去恢复 .sql
错误:
pg_restore appdb.sql
Plain SQL 应该:
psql -d appdb -f appdb.sql
归档格式才使用:
pg_restore -d appdb appdb.dump
错误 2:只备份数据库,不考虑角色
结果恢复时:
role does not exist
如果需要原角色体系:
pg_dumpall --globals-only > globals.sql
如果不需要:
--no-owner --no-acl
错误 3:目标机器没安装扩展
Dump 里面有:
CREATE EXTENSION vector;
但目标 PostgreSQL 根本没有 pgvector。
照样失败。
错误 4:使用太老的 pg_dump
源服务器已经 PostgreSQL 18,本地只有 PostgreSQL 16 的 pg_dump。
不要强行迁。
先安装合适版本的 PostgreSQL Client。
错误 5:备份成功就不管了
最终发现文件已经损坏,或者恢复脚本从几个月前就一直报错。
备份必须有:
生成
→ 校验
→ 异地保存
→ 定期恢复演练
错误 6:备份和数据库放在同一块盘
数据库:
/data/postgresql
备份:
/data/postgresql-backup
然后硬盘坏了。
两个一起没。
这不叫灾备。
至少应该再复制到:
- 另一块盘;
- 另一台服务器;
- 对象存储;
- NAS;
- 异地备份节点。
十八、什么时候应该放弃 pg_dump,转向 PITR?
如果数据库越来越重要,你迟早会遇到这些需求:
RPO 不能是一整天
RTO 不能是几个小时
必须恢复到某个时间点
数据库已经几百 GB / TB
恢复一次逻辑 Dump 太慢
这时候就该开始研究:
pg_basebackup
WAL Archiving
Archive Command
Restore Command
Point-in-Time Recovery
pg_dump 和 PITR 不是互相替代。
很多成熟环境会同时保留:
逻辑备份:pg_dump
+
物理备份:Base Backup
+
持续归档:WAL
逻辑备份方便迁移和提取数据;PITR 负责真正的灾难恢复。
十九、我实际会怎么选
如果只是一个普通项目数据库:
pg_dump -Fc appdb -f appdb.dump
如果需要迁移到另一台服务器且环境不同:
pg_dump -Fc --no-owner --no-acl appdb -f appdb.dump
如果要完整保留角色:
pg_dumpall --globals-only > globals.sql
pg_dump -Fc appdb -f appdb.dump
如果是大数据库:
pg_dump -Fd -j 4 appdb -f appdb_dumpdir
恢复:
pg_restore -j 4 --exit-on-error -d appdb appdb_dumpdir
如果是生产核心数据库,需要恢复到任意时间点:
别只靠 pg_dump。
开始做 WAL + PITR。
最后
PostgreSQL 的备份命令本身并不复杂。
真正复杂的是:
你备份了什么?
有没有漏掉角色?
目标环境有没有扩展?
版本兼不兼容?
文件有没有损坏?
权限能不能恢复?
Sequence 对不对?
业务能不能真正启动?
你有没有真的恢复过一次?
因此我更愿意把数据库备份理解成一个闭环,而不是一条命令:
备份
↓
校验
↓
异地保存
↓
恢复演练
↓
业务验证
↓
确认真的可恢复
做到最后一步,那个 .dump 文件才真正有意义。
参考资料
- PostgreSQL 18 官方文档:pg_dump
- PostgreSQL 18 官方文档:pg_restore
- PostgreSQL 18 官方文档:pg_dumpall
- PostgreSQL 官方文档:Backup and Restore
文中命令用于说明通用流程。正式迁移前请结合自己的 PostgreSQL 版本、扩展、角色体系、数据库大小和停机窗口先进行恢复演练。
PostgreSQL 备份与恢复实战:从 pg_dump、pg_restore 到整库迁移
https://wangling.hauchet.cn/archives/postgresql-backup-restore-pg-dump-pg-restore-migration
评论