把 Go 服务从 MySQL 迁移到 PostgreSQL,看起来只是更换驱动和连接字符串。真正开始以后,差异会从数据类型、SQL、ORM、初始化脚本和运维工具里不断出现。
数据库迁移不是一次编译修复,而是一场分阶段的数据与行为迁移。
一、先做依赖清单
迁移前先搜索:
mysql
MysqlDb
ENGINE=
AUTO_INCREMENT
UNSIGNED
IFNULL
ON DUPLICATE KEY
DATE_FORMAT
需要检查的不只是 .go 文件,还包括:
- 建表 SQL;
- 初始化与升级脚本;
- Docker Compose;
- CI 配置;
- 测试夹具;
- 运维脚本;
- Grafana 或报表查询;
- 文档中的手工操作。
如果全局变量叫 MysqlDb,业务代码已经依赖了具体实现。第一步可以先把它改成中性名称,例如 WebcenterDb,把命名重构和数据库行为迁移分开。
二、数据类型差异
常见映射并不总是一一对应:
| MySQL | PostgreSQL | 注意事项 |
|---|---|---|
TINYINT(1) |
BOOLEAN |
旧数据可能使用 0/1 |
DATETIME |
TIMESTAMP |
是否包含时区 |
JSON |
JSONB |
索引和比较语义 |
AUTO_INCREMENT |
IDENTITY |
序列同步 |
UNSIGNED |
无直接对应 | 改大类型或加约束 |
ENUM |
ENUM/文本 | 迁移与扩展成本 |
BLOB |
BYTEA |
驱动读写方式 |
时间类型尤其需要提前决定:系统究竟存本地时间、UTC,还是带时区时间。迁移工具不会替业务做这个决定。
三、SQL 方言
容易遇到的差异包括:
-- MySQL
IFNULL(value, 0)
-- PostgreSQL
COALESCE(value, 0)
-- MySQL
INSERT ... ON DUPLICATE KEY UPDATE
-- PostgreSQL
INSERT ... ON CONFLICT (...) DO UPDATE
标识符大小写也是常见问题。PostgreSQL 会把未加引号的名称折叠为小写,而带引号的 "DeviceID" 必须保持精确大小写。新表最好统一使用小写蛇形命名。
四、ORM 不能消除数据库差异
GORM 能处理大量基础 CRUD,但下面这些地方仍要专项检查:
- 原生 SQL;
gorm.Expr;- 自动迁移;
- 默认值;
- 索引定义;
- JSON 查询;
- 批量插入;
- upsert;
- 锁语句;
- 分页和排序。
不要因为项目使用 ORM 就跳过 SQL 审查。ORM 只是把差异推迟到更隐蔽的位置。
五、约束可能暴露旧问题
PostgreSQL 通常会让一些过去被宽松接受的数据直接失败,例如:
- 空字符串写入数字字段;
- 非法时间;
- 超长字符串;
- 外键不一致;
- 布尔值使用任意整数;
GROUP BY中选择未聚合字段。
这不一定是迁移制造了问题,更可能是新数据库让旧数据问题显形。应该先统计和清洗,而不是关闭约束继续迁移。
六、迁移策略
方案一:停机迁移
适合数据量较小、可接受维护窗口的系统。
停止写入
-> 导出数据
-> 转换并导入
-> 校验
-> 切换配置
-> 启动服务
优点是简单,缺点是停机时间受数据量影响。
方案二:双写迁移
应用同时写入两个数据库,历史数据后台同步,验证完成后切换读取。
双写并不天然可靠。需要处理部分成功、重试、顺序和一致性,复杂度很高。
方案三:变更数据捕获
通过 binlog 等机制同步增量,适合数据量大、停机要求高的场景,但基础设施和运维成本更高。
选择策略时,不要只看“是否零停机”,还要看团队是否有能力观察和修复迁移过程。
七、数据校验
只比较总行数远远不够。
可以分层校验:
- 每张表行数;
- 主键范围;
- 关键字段空值数量;
- 按时间或 ID 分片计算校验和;
- 抽样比较完整记录;
- 执行核心业务查询并比较结果;
- 比较接口响应。
金额、时间、JSON 和浮点数要使用符合业务语义的比较方式,不能简单转成字符串。
八、序列问题
导入带主键的数据后,PostgreSQL 序列值可能仍停留在初始位置,下一次插入就会主键冲突。
迁移后需要把序列推进到当前最大值,并把这一步写入自动化脚本,不能依赖人工记忆。
九、性能基线
迁移前后应比较:
- 核心接口延迟;
- 慢查询;
- 连接池使用率;
- CPU 与 I/O;
- 索引命中率;
- 批量写入速度;
- 锁等待;
- 数据库体积。
同一条 SQL 在两个数据库上的执行计划可能完全不同。迁移成功的标准不是“能查询”,而是关键负载下行为符合预期。
十、回滚
切换前必须明确:
- 回滚窗口有多长;
- 切换后产生的新数据如何回到 MySQL;
- 哪个数据库是切换期间的事实源;
- 什么指标触发回滚;
- 谁做最终决定。
如果切换后 PostgreSQL 已接受新写入,简单修改连接字符串并不能完成回滚。
十一、上线清单
- [ ] 代码中不再依赖 MySQL 命名;
- [ ] 数据类型映射已经确认;
- [ ] 原生 SQL 和 ORM 特殊用法已检查;
- [ ] 历史脏数据已清理;
- [ ] 序列已同步;
- [ ] 核心数据完成分层校验;
- [ ] 性能基线已对比;
- [ ] 连接池与超时已配置;
- [ ] 备份和恢复经过演练;
- [ ] 切换与回滚步骤已经自动化;
- [ ] 迁移期间的事实源已经明确;
- [ ] 监控和告警已覆盖新数据库。
数据库迁移真正迁移的不是表,而是系统对数据类型、约束、事务和故障恢复的一整套假设。把这些假设逐一显式化,迁移才不只是“换一个能跑的驱动”。