把 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 等机制同步增量,适合数据量大、停机要求高的场景,但基础设施和运维成本更高。

选择策略时,不要只看“是否零停机”,还要看团队是否有能力观察和修复迁移过程。

七、数据校验

只比较总行数远远不够。

可以分层校验:

  1. 每张表行数;
  2. 主键范围;
  3. 关键字段空值数量;
  4. 按时间或 ID 分片计算校验和;
  5. 抽样比较完整记录;
  6. 执行核心业务查询并比较结果;
  7. 比较接口响应。

金额、时间、JSON 和浮点数要使用符合业务语义的比较方式,不能简单转成字符串。

八、序列问题

导入带主键的数据后,PostgreSQL 序列值可能仍停留在初始位置,下一次插入就会主键冲突。

迁移后需要把序列推进到当前最大值,并把这一步写入自动化脚本,不能依赖人工记忆。

九、性能基线

迁移前后应比较:

  • 核心接口延迟;
  • 慢查询;
  • 连接池使用率;
  • CPU 与 I/O;
  • 索引命中率;
  • 批量写入速度;
  • 锁等待;
  • 数据库体积。

同一条 SQL 在两个数据库上的执行计划可能完全不同。迁移成功的标准不是“能查询”,而是关键负载下行为符合预期。

十、回滚

切换前必须明确:

  • 回滚窗口有多长;
  • 切换后产生的新数据如何回到 MySQL;
  • 哪个数据库是切换期间的事实源;
  • 什么指标触发回滚;
  • 谁做最终决定。

如果切换后 PostgreSQL 已接受新写入,简单修改连接字符串并不能完成回滚。

十一、上线清单

  • [ ] 代码中不再依赖 MySQL 命名;
  • [ ] 数据类型映射已经确认;
  • [ ] 原生 SQL 和 ORM 特殊用法已检查;
  • [ ] 历史脏数据已清理;
  • [ ] 序列已同步;
  • [ ] 核心数据完成分层校验;
  • [ ] 性能基线已对比;
  • [ ] 连接池与超时已配置;
  • [ ] 备份和恢复经过演练;
  • [ ] 切换与回滚步骤已经自动化;
  • [ ] 迁移期间的事实源已经明确;
  • [ ] 监控和告警已覆盖新数据库。

数据库迁移真正迁移的不是表,而是系统对数据类型、约束、事务和故障恢复的一整套假设。把这些假设逐一显式化,迁移才不只是“换一个能跑的驱动”。