将 PostgreSQL 迁移到另一种关系型数据库,表面上是导出数据、导入数据、修改连接串,实际上更接近一次系统级兼容性改造。数据库对象、SQL 语法、事务隔离、时间类型、序列、索引和驱动行为都可能影响结果。
最容易被忽略的是:目标库能够启动应用,并不代表迁移完成。应用可能只在低并发下正常运行;某些报表 SQL 可能因函数差异返回不同结果;原本依赖隐式类型转换的写法,可能在目标库中直接报错。即使查询结果一致,执行计划改变也可能让核心接口出现延迟抖动。
因此,迁移目标应拆成四个可验证的条件:
- 结构完整,业务对象能够创建并被应用访问。
- 数据完整,行数、关键字段和业务汇总结果一致。
- 行为兼容,事务、分页、时间处理和异常语义符合应用预期。
- 性能可接受,并且出现问题时能够回到旧库。
本文不假定某个具体国产数据库版本具备全部 PostgreSQL 兼容能力。示例中的语法和工具参数需要结合目标产品文档进行调整,尤其要关注扩展、存储过程和驱动支持范围。
二、先做资产盘点,再决定迁移路线
迁移前不要直接执行全量导出。先从生产或预生产环境生成资产清单,至少包括表、视图、序列、索引、约束、函数、触发器、扩展、定时任务和应用 SQL。
可以先在 PostgreSQL 中查询基础对象:
SELECT n.nspname AS schema_name,
c.relname AS object_name,
c.relkind AS object_type
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY n.nspname, c.relkind, c.relname;
同时记录这些信息:
- 单表行数、总数据量和最大单表大小。
- 主键是否存在,是否有重复候选键。
- 外键关系和删除更新规则。
- 使用了哪些扩展,例如全文检索、地理空间或特定 UUID 能力。
- 函数和触发器依赖的语言、系统表或扩展函数。
- 应用中出现的 PostgreSQL 专用语法,例如
RETURNING、ON CONFLICT、数组操作符、JSONB 运算符和ILIKE。
盘点结果决定路线。若目标数据库对 PostgreSQL 语法兼容度较高,可以采用“转换 DDL 加批量迁移”;若存在大量扩展、函数或复杂报表,则应先建立应用适配层,并把业务切换拆成多个阶段。不要把“迁移工具能处理的对象”误认为“系统全部兼容”。
三、建立类型映射表,避免隐式转换埋雷
类型映射应由业务语义驱动,而不是只按名称替换。例如:
| PostgreSQL 类型 | 迁移时重点关注 | 建议验证方式 |
|---|---|---|
bigint |
是否被应用当作 JavaScript 安全整数处理 | 检查最大值和序列增长策略 |
numeric(p,s) |
精度、舍入和空值行为 | 对金额进行边界计算 |
timestamp with time zone |
时区存储和展示规则 | 使用多个时区做往返测试 |
jsonb |
索引、路径查询和排序行为 | 验证查询结果及索引使用 |
bytea |
二进制编码与驱动返回格式 | 校验哈希值 |
uuid |
原生类型或字符存储差异 | 验证格式、大小写和索引 |
| 数组类型 | 是否需要拆表或序列化 | 验证空数组、空值和元素顺序 |
如果目标库没有等价的原生类型,应明确记录转换规则。例如将数组序列化为 JSON 后,原来的数组包含查询不能直接照搬;将带时区时间转为无时区字段,也必须规定统一存储时区,否则迁移后会出现按天统计偏移。
四、分层迁移:DDL、数据和应用代码分别处理
1. 转换并审查 DDL
不要直接把 PostgreSQL 的完整备份文件当作目标库脚本。备份文件可能包含目标库不认识的扩展、权限、拥有者、表空间和专用语法。
建议生成一份中间 DDL,然后按以下顺序审查:
- Schema 和基础类型。
- 表和默认值。
- 主键及唯一约束。
- 数据导入。
- 普通索引。
- 外键、触发器和复杂函数。
把外键和部分触发器放到数据导入之后,通常能减少导入阶段的依赖问题,但这不是通用规则。若业务要求导入过程中始终保持强约束,就应使用目标库支持的约束检查策略,并单独评估导入性能。
2. 使用批量数据迁移并保留断点
迁移程序应具备分批、限速、重试和断点记录能力。可以按主键范围或时间分片,而不要依赖不稳定的偏移分页。
下面是一个简化的 Python 迁移骨架。它展示的是工程边界,不绑定某个具体数据库驱动:
import os
import time
import hashlib
import psycopg
SOURCE_DSN = os.environ["SOURCE_DSN"]
TARGET_DSN = os.environ["TARGET_DSN"]
BATCH_SIZE = int(os.getenv("BATCH_SIZE", "2000"))
def row_fingerprint(row):
payload = "\x1f".join("<NULL>" if v is None else str(v) for v in row)
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
def migrate_orders():
last_id = 0
with psycopg.connect(SOURCE_DSN) as source, psycopg.connect(TARGET_DSN) as target:
while True:
rows = source.execute(
"""
SELECT id, customer_id, amount, created_at
FROM orders
WHERE id > %s
ORDER BY id
LIMIT %s
""",
(last_id, BATCH_SIZE),
).fetchall()
if not rows:
break
with target.transaction():
for row in rows:
target.execute(
"""
INSERT INTO orders(id, customer_id, amount, created_at)
VALUES (%s, %s, %s, %s)
ON CONFLICT (id) DO UPDATE SET
customer_id = EXCLUDED.customer_id,
amount = EXCLUDED.amount,
created_at = EXCLUDED.created_at
""",
row,
)
last_id = rows[-1][0]
time.sleep(float(os.getenv("BATCH_INTERVAL", "0")))
实际使用前,需要确认目标库是否支持示例中的冲突处理语法;不支持时,应改为目标库对应的幂等写入方式。断点不能只保存在进程内,应该记录到独立的迁移控制表中,并包含表名、分片边界、批次状态、开始时间、结束时间和错误摘要。
3. 对应用 SQL 做显式改造
应用代码中不要把数据库差异散落到各个业务方法。可以在仓储层或 SQL 适配层集中处理分页、时间、冲突写入和批量参数绑定。
例如,原有分页如果使用大偏移量:
SELECT id, status, created_at
FROM orders
ORDER BY id
LIMIT :page_size OFFSET :offset;
在大表上可能既慢又容易受到数据变化影响。可以改为基于游标的分页:
SELECT id, status, created_at
FROM orders
WHERE id > :last_id
ORDER BY id
FETCH FIRST :page_size ROWS ONLY;
FETCH FIRST 是否支持、参数占位符如何书写,要以目标数据库驱动为准。重点是固定排序列、使用稳定游标,并在应用层封装差异。
五、验证不能只比总行数
1. 结构验证
分别导出两端的表、列、类型、默认值、主键、唯一约束和索引清单,做规范化比较。比较时忽略自动生成的对象名称差异,但不能忽略约束语义差异。
2. 数据验证
总行数只能发现最明显的问题。建议按主键范围或日期分区计算摘要:
SELECT date_trunc('day', created_at) AS day,
COUNT(*) AS row_count,
SUM(amount) AS amount_sum,
MIN(id) AS min_id,
MAX(id) AS max_id
FROM orders
GROUP BY date_trunc('day', created_at)
ORDER BY day;
目标库不一定支持 date_trunc,应使用等价日期函数。金额字段还要比较精度和舍入结果;文本字段要检查字符集、排序规则和尾随空格;二进制字段可以比较 SHA-256 摘要。对关键表可抽样比较完整行,但抽样规则必须固定并可重复。
3. 业务验证
准备一组脱离生产数据的验收场景:创建订单、取消订单、重复提交、跨时区查询、分页翻页、并发更新、事务回滚、报表汇总和权限校验。每个场景记录输入、预期结果、两端实际结果以及允许的差异。
4. 性能验证
性能验证应使用接近真实的数据规模、索引和查询参数。至少观察核心 SQL 的执行计划、响应时间分布、锁等待、连接数和日志错误。没有同等硬件、数据量或流量条件时,不应直接宣称迁移后性能提升;最多只能说明在当前验证环境中未发现明显回退。
六、切换与回滚设计
一种较稳妥的切换流程如下:
- 先完成全量迁移和离线校验。
- 在旧库继续提供服务,同时将新增或变更数据同步到目标库。
- 对目标库执行只读回放和业务验收。
- 进入短暂维护窗口,暂停写入,等待同步追平。
- 比较最后一批变更,备份应用配置。
- 先将少量实例切换到目标库,观察错误率、延迟、连接池和数据摘要。
- 分批扩大流量,保留旧库只读或可恢复状态。
回滚必须提前定义触发条件。例如核心接口错误率超过阈值、关键汇总不一致、锁等待持续升高或出现无法解释的数据变化。若目标库已经接受写入,直接把连接串改回旧库并不一定安全,因为两端数据可能分叉。更可靠的回滚方案是保留变更日志、反向同步能力,或在切换期间限制写入并准备可验证的恢复点。
配置应通过环境变量管理,示例:
export SOURCE_DSN='postgresql://user:password@old-db:5432/app'
export TARGET_DSN='targetdb://user:password@new-db:5432/app'
export BATCH_SIZE='2000'
export BATCH_INTERVAL='0.05'
密码不应写入代码、脚本仓库或日志。生产环境还应使用密钥管理系统,并限制迁移账号权限:读取源库所需对象,写入目标库所需对象,以及记录迁移状态所需的最小权限。
七、常见问题
迁移工具能自动转换全部 SQL 吗?
不能。工具通常擅长表结构或基础数据转换,复杂函数、扩展、触发器、动态 SQL 和应用拼接 SQL 仍需人工审查。自动转换后的脚本必须经过目标库解析和业务测试。
为什么数据行数一致,业务结果仍然不同?
常见原因包括时区转换、数值精度、排序规则、空值处理、重复键覆盖以及触发器未执行。应按分区比较摘要,并用业务场景验证,而不是只看总行数。
是否应该一次性切换全部应用实例?
除非系统规模很小且回滚路径已经演练,否则不建议。分批切换可以缩小故障范围,但前提是应用版本和数据库访问逻辑能够在过渡期内兼容。
什么时候可以删除旧库?
至少等到业务核对完成、备份可恢复性验证通过、回滚窗口结束,并满足组织规定的审计和保留周期。旧库不应因为“新库已经能用”就立即销毁。
总结
数据库迁移的核心不是把数据搬到另一台服务器,而是证明迁移前后的结构、数据和业务行为满足同一份验收标准。可执行的路线通常包含资产盘点、类型映射、分层迁移、可重复校验、灰度切换和明确回滚。对不确定的兼容行为,应先在目标版本的隔离环境中验证,并把结果固化为自动化检查。这样迁移才从一次高风险操作,变成可观察、可暂停、可恢复的工程流程。