数据库备份最危险的误区,不是“没有备份”,而是看到磁盘上躺着一个 .sql.dump 文件,就默认自己已经有了可恢复的备份

真正出事的时候,你才会发现:角色没备份、扩展没装、Owner 对不上、目标库已经有对象、备份文件损坏、版本不兼容,甚至从来没有真正执行过一次恢复。

这篇文章不只讲两个命令,而是完整走一遍 PostgreSQL 最常见的一条迁移链路:

源数据库 → pg_dump → 备份文件 → 传输 → 新 PostgreSQL → pg_restore / psql → 校验 → 切换业务

本文主要面向日常开发、服务器迁移、Docker 数据库迁移和中小规模生产环境。命令按 PostgreSQL 18 官方文档核对,但大多数内容同样适用于 PostgreSQL 14~18。

PostgreSQL pg_dump / pg_restore 工作流

上图来自 Wikimedia Commons。左边是 Plain SQL + psql,右边是归档格式 + pg_restore。这两个恢复路径一定要分清。


一、先分清:pg_dump 到底是不是“完整备份”

PostgreSQL 官方把备份方式大致分成三类:

  1. SQL Dump,也就是本文主要讲的 pg_dump / pg_dumpall
  2. 文件系统级备份;
  3. 基于 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 版本、扩展、角色体系、数据库大小和停机窗口先进行恢复演练。