MySQL2PG 异构数据库转换实战:从 MySQL 到 PostgreSQL 的表结构与数据迁移
1. MySQL2PG 异构数据库转换:为什么手动改 DDL 总会踩坑
MySQL 到 PostgreSQL 的迁移,很多人第一反应是「用 pgloader 或者 Navicat 导一下不就行了」。真到业务库上,你会发现事情没那么简单:tinyint(1)到了 PG 变成 smallint 而不是 boolean,AUTO_INCREMENT要换成GENERATED BY DEFAULT AS IDENTITY,反引号要全删,ENGINE=InnoDB和CHARSET=utf8mb4这些子句 PG 根本不认。表一多,几百上千张表,靠手工改 DDL 基本是体力活,而且改错一个字段类型,后面数据同步就会报类型不匹配。
MySQL2PG 就是冲着这个痛点来的:它是一个用 Go 写的命令行工具,读取 MySQL 的表定义,自动做字段类型映射,在 PostgreSQL 里建表,然后分批把数据搬过去,再重建索引、同步用户和表权限,最后做行数校验。适合谁?适合手上有几十到上千张表的 MySQL 业务库、想整体迁到 PostgreSQL、又不想逐表写转换脚本的开发者。它不替代你的编辑器,也不做在线双写,定位是一次可复现的库级迁移演练。
我试过拿一个 1300 多张表的库跑全流程,最直观的感受是「进度可视化」这件事很关键——你能看到第几张表、同步了多少行、哪张表校验不一致,而不是干等一个黑盒。下面按「准备配置 → 跑起来 → 验证 → 排错」的顺序,把可复制的步骤拆开讲。
先明确几个概念,避免后面看配置发懵。表结构转换指把 MySQL 的CREATE TABLE翻译成 PG 能执行的 DDL;数据同步指按批把行读出来插到 PG;索引转换指主键、唯一索引、普通索引在 PG 侧重建;用户与权限转换指把 MySQL 的账号和 GRANT 映射成 PG 的 role 和 GRANT。这四件事在配置文件里是独立开关,你可以只开表结构、只开数据,也可以全开跑一遍完整迁移。
还有一个容易忽略的点:MySQL 和 PostgreSQL 的标识符大小写规则不同。MySQL 在 Linux 上表名默认区分大小写,PG 则会把未加引号的标识符全部折叠成小写。所以工具里有个lowercase_columns开关,控制字段名是否统一转小写。如果你的业务代码里有驼峰字段名,迁移前一定要想清楚这一层,否则应用连上来会找不到列。
2. 前置准备:TaoToken 配置与 MySQL2PG 运行环境
这一节分两块:一块是 MySQL2PG 本身的运行环境,一块是如果你打算用 AI 辅助写迁移脚本、生成校验 SQL 时,怎么把模型调用配好。两块互不依赖,按需取用。
2.1 MySQL2PG 的环境与获取
工具是 Go 写的,最省事的方式是直接拿编译好的二进制,跨平台、启动快、内存占用低,这也是原作者选 Go 的原因。你需要准备:
- 一个能连通的 MySQL 实例(测试用 5.7.x 即可,生产建议先确认版本)
- 一个 PostgreSQL 实例(16.x 实测可用)
- 目标 PG 库里提前建好 database,工具只建表不建库
- MySQL 账号需要有读取表结构、读取数据、读取
mysql.user和权限表的权限
源码仓库在 GitHub 上(xfg0218/MySQL2PG),你可以git clone后自己go build,也可以直接用 release 里的二进制。编译命令很直接:
git clone https://github.com/xfg0218/MySQL2PG.git cd MySQL2PG go mod tidy go build -o mysql2pg .编译完当前目录会多一个mysql2pg可执行文件。运行前确认config.yml和它在同一目录,工具默认读当前路径的配置,日志也写在当前路径。
2.2 用 TaoToken 辅助生成迁移与校验脚本
迁移过程中经常要临时写一些校验 SQL,比如「对比两张表在 MySQL 和 PG 里的行数」「找出 PG 里缺失的索引」。这类一次性脚本用模型生成很合适。TaoToken 提供统一的模型调用入口,把 Base URL 指向它、填上 Key、选好 Model ID 就能用,不用分别去各家平台开账号。
三件套这样配:Base URL 填https://taotoken.net/api,API Key 在控制台的 API Keys 页面创建,Model ID 按你选的模型填(比如对话类或代码类模型的具体名称)。如果你用的是 Claude Code 这类编码工具,走的是 Anthropic 兼容入口,配置方式略有不同,参考文档里的 ClaudeCodeAnthropic 说明即可。
需要提醒的是:模型调用只用来生成辅助脚本和排查思路,真正的迁移执行还是靠 MySQL2PG 本地跑,别把生产库连接信息贴进任何对话里。生成完脚本自己 review 一遍再执行,这是基本习惯。
3. 可复制配置:config.yml 逐项拆解
MySQL2PG 的所有行为都由config.yml驱动。下面这份配置是按「全流程跑一遍」写的,你可以按需关掉某些阶段。注意 YAML 的缩进,层级错了工具会读不到。
mysql: host: "127.0.0.1" port: 3306 user: "migrate_user" password: "your_mysql_password" database: "biz_db" charset: "utf8mb4" test_only: false postgresql: host: "127.0.0.1" port: 5432 user: "postgres" password: "your_pg_password" database: "biz_db_pg" sslmode: "disable" test_only: false conversion: tableddl: true data: true indexes: true functions: false users: true table_privileges: true validate_data: true lowercase_columns: true skip_existing_tables: true truncate_before_sync: true use_table_list: false table_list: - "user" - "orders" max_rows_per_batch: 1000 batch_insert_size: 500 concurrency: 4 max_indexes_per_batch: 20逐项说关键参数。test_only放在 mysql 和 postgresql 两个块里,任意一个设为 true,工具就只测对应连接、打印版本信息然后退出,适合迁移前先确认网络和账号没问题。tableddl控制建表,skip_existing_tables为 true 时目标已存在的表会跳过而不是报错,重复跑很友好。truncate_before_sync建议第一次迁移设为 true,先清空目标表再灌数据,避免主键冲突;如果你做的是增量补数,设 false 但要接受可能出现重复行。
use_table_list配合table_list用,只同步你关心的表,适合先拿几张核心表试水。max_rows_per_batch是每次从 MySQL 读多少行,batch_insert_size是每批往 PG 插多少行,concurrency是并发线程数。这三个值要根据你的网络延迟和库负载调,默认值偏保守,表特别大时可以适当加大,但别一上来就拉满。
lowercase_columns前面提过,统一转小写能规避大小写折叠问题,但如果你的应用层 SQL 里写死了带引号的驼峰列名,就要设 false 并自己保证一致性。functions目前版本对 MySQL FUNCTION 的自动转换还不完整,建议先关掉,函数单独处理。
配置写完后,先跑一次只测连接的:
./mysql2pg如果两个test_only都是 false,它会直接进入转换流程。想单独验证连接,把其中一个设 true 再跑,控制台会打印类似这样的版本对照表:
+--------------+----------------------------------------------+ | 数据库类型 | 版本信息 | +--------------+----------------------------------------------+ | MySQL | 5.7.44 | | PostgreSQL | PostgreSQL 16.1 on x86_64-pc-linux-gnu... | +--------------+----------------------------------------------+看到这张表,说明账号、网络、库名都对上了,可以往下走。
4. 执行迁移与验证:从建表到行数一致性校验
配置确认无误后,直接运行./mysql2pg,工具会按 tableddl → data → indexes → users → table_privileges → validate_data 的顺序执行。控制台会输出带百分比的进度,比如「进度: 17.24% (5/29) : 同步表 acl_space 数据成功,共有 0 行数据,数据校验一致」。这个百分比是按对象总数算的,表多的时候能直观看到还剩多少。
4.1 分阶段观察输出
表结构阶段,已存在的表会提示「表 xxx 已存在,跳过创建」,这是skip_existing_tables生效。数据同步阶段,每张表同步完会立刻做一次行数比对,一致就标「数据校验一致」。索引阶段会逐条打印「转换索引 idx_user_id 成功」。用户和权限阶段会打印「转换用户 user1@% 的权限成功」这类信息。
全部跑完后,控制台会给一张各阶段耗时汇总表:
+--------------------------+----------------+-----------------------+ | 阶段 | 对象数量 | 耗时(秒) | +--------------------------+----------------+-----------------------+ | 转换表结构 | 1339 | ... | | 同步表数据 | 1339 | ... | | 转换表索引 | 1768 | ... | | 转换库用户 | 5 | ... | | 转换表权限 | 15 | ... | +--------------------------+----------------+-----------------------+这张表是你评估迁移窗口的依据。表越多、数据量越大,data 阶段占比越高,可以针对性调concurrency和批大小。
4.2 迁移后的一致性验证动作
工具自带的validate_data只比对行数,这是第一道关,但不够。行数一致不代表内容一致,建议再补两步手工校验。
第一步,抽样比对关键表的字段值。在 MySQL 和 PG 各跑一条聚合 SQL,比如对订单表按状态分组计数:
-- MySQL 侧 SELECT status, COUNT(*) AS cnt FROM orders GROUP BY status ORDER BY status; -- PostgreSQL 侧 SELECT status, COUNT(*) AS cnt FROM orders GROUP BY status ORDER BY status;两边结果贴出来对一遍,数量级和分组分布一致,基本能排除大批量错位。
第二步,检查索引是否齐全。在 PG 里查目标表的索引清单:
SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'orders';对照 MySQL 的SHOW INDEX FROM orders;,确认主键、唯一索引、复合索引都在。复合索引的列顺序尤其要看,顺序错了查询计划会用不上。
第三步,验证自增列。MySQL 的AUTO_INCREMENT迁到 PG 后是 identity 列,插入一条测试数据看主键是否自动递增:
INSERT INTO orders (user_id, amount) VALUES (1, 99.00) RETURNING id;能返回一个新 id 就说明序列接上了。如果报「null value in column id violates not-null」,说明 identity 没建对,需要回表结构阶段排查。
4.3 指定表同步的用法
不想全量跑,就把use_table_list设 true,table_list里列上表名。工具只处理这些表,进度分母也会变成列表长度。实测两张表的数据同步不到一秒,适合反复调试某几张核心表的转换规则。
5. 常见报错排查:401、duplicate key 与表不存在
迁移过程里的报错大致分三类:连接认证类、数据冲突类、对象缺失类。下面按真实日志对照说。
认证失败(401 类)。如果你在辅助脚本里调模型接口报 401,先查 API Key 是否填对、有没有多余空格、Base URL 是不是https://taotoken.net/api而不是带路径的完整端点。MySQL2PG 本身的连接失败则表现为「Access denied for user」,检查config.yml里的账号密码和 MySQL 的 host 授权,user@%和user@localhost是两条不同的授权记录。
duplicate key value violates unique constraint。这是数据同步阶段最常见的错,日志长这样:
ERROR: 插入表 users_20251201 数据失败: 批量插入失败: ERROR: duplicate key value violates unique constraint "users_20251201_pkey" (SQLSTATE 23505)原因通常是truncate_before_sync设成了 false,目标表里已有旧数据,新数据主键撞上了。解决办法二选一:把truncate_before_sync改 true 重跑,或者手工清空目标表再同步。如果你确实要做增量,得自己保证源端只取新增行,工具本身不做去重。
表在 PostgreSQL 中不存在,跳过权限授予。日志里会出现「表 users 在PostgreSQL中不存在,跳过权限授予」。这说明权限转换阶段找不到对应表,多半是表结构阶段跳过了这张表(skip_existing_tables或建表失败),或者table_list里没包含它。先确认 PG 里\dt能看到这张表,再看表结构阶段的日志有没有报错。
数据量校验不一致。当truncate_before_sync为 false 时,工具不会因行数不一致中断,而是在结尾输出一张统计表:
+------------------+----------------+------------------+ | 表名 | MySQL数据量 | PostgreSQL数据量 | +------------------+----------------+------------------+ | users_20251201 | 1 | 2 | +------------------+----------------+------------------+PG 侧多出来的行就是重复插入的。按表名去 PG 里查主键分布,删掉重复行,或者干脆清表重跑。
日志去哪看。工具默认在当前目录生成errors.log和conversion.log。errors.log只记错误,适合快速定位失败点;conversion.log记全过程,包括「表 xxx 已存在,跳过创建」这种信息级日志。排查时先看 errors.log 的最后一条,再回 conversion.log 找上下文。
字段类型映射不符预期。比如 MySQL 的tinyint(1)你期望变 boolean,结果成了 smallint。这跟工具版本和映射规则有关,建表后手工ALTER TABLE ... ALTER COLUMN ... TYPE boolean USING ...修正,或者迁移前在 MySQL 侧把字段定义调整好。批量修正可以写个脚本遍历information_schema.columns生成 ALTER 语句。
6. 把迁移流程固化成可复现的演练
一次成功的迁移不算完,能重复跑才是关键。建议把整个流程固化成三步:先在测试环境用test_only验证连通性,再用use_table_list挑三五张核心表跑通全流程,最后全量跑并留存conversion.log作为迁移记录。每次改配置后重跑,skip_existing_tables和truncate_before_sync的组合决定了是「增量补」还是「清空重灌」,这个要想清楚再执行。
如果你在迁移过程中需要批量生成校验 SQL、或者让模型帮你解读某段报错日志,可以把模型调用配到 TaoToken 的 API 入口,Base URL 用https://taotoken.net/api,Key 在控制台创建,Model ID 按需选。长期做编码和 Agent 类任务的,可以看看 Coding Plan 的额度方案;只是偶尔验证模型效果的,直接用模型对话页面试就行。接入细节和参数说明在接入文档里有完整示例,照着填三件套即可。
最后留一个实操建议:迁移前在 MySQL 侧执行SHOW CREATE TABLE把关键表的原始 DDL 存一份,迁移后在 PG 侧用pg_dump --schema-only导一份对比。两份 DDL 摆在一起看,字段类型、默认值、索引的差异一目了然,比事后靠应用报错反推高效得多。