MySQL到PG迁移指南:CI流水线实现平滑切换
摘要
MySQL2PG通过CI流水线实现MySQL到PostgreSQL平滑迁移,测试覆盖167张表,涵盖基础类型、索引约
从MySQL迁移到PostgreSQL这事儿,表面上看好像只是换个数据库软件,但真正上手做过的人都知道,这里面的坑远比想象中要多。
AUTO_INCREMENT要改成SERIAL,IFNULL得换成COALESCE,GROUP_CONCAT对应的是string_agg,更别提分区表语法完全两码事、存储过程逻辑要一条条适配、各种索引和外键约束一个都不能错——只要有一个环节出了岔子,数据就可能丢了、查不出来、甚至根本写不进去。
今天聊的这个开源项目MySQL2PG,其核心思路是:用一套成体系的自动化测试流程,把“心跳式迁移”变成“安心迁移”。说白了,就是把大量潜在的迁移问题提前暴露在CI流水线里,而不是等到生产环境出问题才去排查。
一、167张测试表:从基础类型到业务场景的全覆盖
在create_table.sql文件中,定义了167个测试表案例,按覆盖范围分成了五大类。
第一类:基础类型与DDL语法(case_01 ~ case_40)
这一组主要覆盖数值、字符集、JSON、时间、默认值、自增、约束、生成列、保留字和命名风格等基本要素。
| 测试范围 | 表案例 | 覆盖内容 |
|---|---|---|
| 整数类型 | case_01 | tinyint/smallint/mediumint/int/bigint/integer,含精度变体 |
| 布尔类型 | case_02 | TINYINT(1) → BOOLEAN,大小写不敏感 |
| 浮点数类型 | case_03 | float/double/decimal/numeric/real,含精度和标度 |
| 字符集类型 | case_04~07 | utf8/utf8mb4/latin1/utf16/ascii,含排序规则 |
| JSON类型 | case_08 | json字段,支持嵌套结构 |
| 日期时间类型 | case_09 | date/time/datetime/timestamp/year,含精度变体 |
| 默认值变体 | case_10 | 数值默认值、字符串默认值、CURRENT_TIMESTAMP |
| 自增类型 | case_11 | AUTO_INCREMENT,多自增字段处理 |
| 无符号类型 | case_12 | unsigned/zerofill,无符号整数转换 |
| 枚举和集合 | case_13 | enum/set → VARCHAR(255) |
| 二进制类型 | case_14 | binary/varbinary/blob/longblob/mediumblob/tinyblob → BYTEA |
| 表选项 | case_15 | ROW_FORMAT/COLLATE/CHARSET 等表级选项 |
| 分区表 | case_16 | RANGE 分区,按年份分区 |
| 临时表 | case_17 | TEMPORARY TABLE 处理 |
| 引号标识符 | case_18 | 反引号引用标识符 |
| 注释类型 | case_19 | 列注释和表注释 |
| 约束类型 | case_20 | PRIMARY KEY/UNIQUE KEY/INDEX 复合约束 |
| 虚拟列 | case_21 | GENERATED ALWAYS AS VIRTUAL |
| 空间类型 | case_22 | geometry/point/linestring/polygon 等 |
| 怪异语法 | case_23 | INTEGER(10)/DOUBLE PRECISION(10,2) 等非标准语法 |
| 边缘情况 | case_24 | 混合字符集、自增主键、longblob |
| MySQL 8.0保留字 | case_25 | rank/system/groups/window/function/role/admin |
| 不可见列 | case_26 | INVISIBLE COLUMN 和不可见索引 |
| 检查约束 | case_27 | CHECK (age > 18) 约束 |
| 函数索引 | case_28 | MySQL 8.0 表达式索引 |
| 默认值变体 | case_29 | char/json 默认值 |
| 字符集和排序规则 | case_30 | utf8mb4_general_ci/utf8mb4_bin |
| 系统表模拟 | case_31 | 模拟 mysql.db 表结构 |
| 复杂生成列 | case_32 | CASE WHEN 表达式的生成列 |
| 降序索引 | case_33 | DESC 索引,混合方向主键 |
| 表选项 | case_34 | ENGINE=InnoDB 显式指定 |
| 枚举字符集 | case_35 | enum/set 带字符集和排序规则 |
| 大写表名 | case_36 | UPPERCASE 表名和列名 |
| 驼峰命名 | case_37 | ProductId/ProductName/LastUpdate |
| 蛇形命名 | case_38 | product_id/product_name/last_update |
| 下划线命名 | case_39 | 带下划线的命名风格 |
| 默认值 | case_40 | 各种默认值变体 |
第二类:索引/约束与表特性(case_41 ~ case_80)
这一组把目光投向外键、全文索引、空间索引、复合主键、存储引擎、分区策略、表复制、压缩表、统计信息等更进阶的特性。
| 测试范围 | 表案例 | 覆盖内容 |
|---|---|---|
| 外键约束 | case_41 | 父子表关联,ON DELETE CASCADE/ON UPDATE SET NULL |
| 全文索引 | case_42 | FULLTEXT KEY,支持自然语言搜索 |
| 空间索引 | case_43 | SPATIAL INDEX,地理空间数据 |
| 复合主键 | case_44 | 多列组合主键 |
| 存储生成列 | case_45 | STORED/VIRTUAL 生成列 |
| MyISAM引擎 | case_46 | MyISAM 存储引擎转换 |
| MEMORY引擎 | case_47 | MEMORY 存储引擎转换 |
| 索引类型 | case_48 | BTREE/HASH 索引 |
| LIST分区 | case_49 | PARTITION BY LIST |
| HASH分区 | case_50 | PARTITION BY HASH,4个分区 |
| 表复制LIKE | case_51 | CREATE TABLE ... LIKE |
| 表复制AS | case_52 | CREATE TABLE ... AS SELECT |
| 延迟约束 | case_53 | 延迟约束验证 |
| 表空间 | case_54 | TABLESPACE 指定 |
| 压缩表 | case_55 | ROW_FORMAT=COMPRESSED |
| 加密表 | case_56 | 透明数据加密 |
| 列级权限 | case_57 | 敏感数据列权限 |
| 子分区 | case_58 | RANGE + HASH 复合分区,12个子分区 |
| 复杂生成列 | case_59 | 多函数表达式生成列 |
| 统计信息 | case_60 | STATS_PERSISTENT/STATS_AUTO_RECALC |
| 大量列 | case_61 | 60 列宽表,覆盖所有MySQL类型 |
| 各种默认值 | case_62 | ON UPDATE CURRENT_TIMESTAMP |
| 字符集排序规则 | case_63 | 多字符集混合 |
| BIT类型 | case_64 | bit(1)/bit(8)/bit(16)/bit(32)/bit(64) |
| YEAR类型 | case_65 | year(4)/year 默认值 |
| 空间子类型 | case_66 | geometry/point 带注释 |
| 触发器模拟 | case_67 | created_at/updated_at 自动更新 |
| 视图模拟 | case_68 | 模拟视图结构的表 |
| 深层嵌套JSON | case_69 | config/tags/metadata JSON字段 |
| utf8mb4_900排序 | case_70 | MySQL 8.0 新排序规则 |
| 函数索引 | case_71 | concat 表达式索引 |
| 检查约束正则 | case_72 | CHECK (email LIKE '%@%') |
| 混合生成列 | case_73 | STORED + VIRTUAL 混合 |
| 混合可见性列 | case_74 | 可见/不可见列混合 |
| 降序主键 | case_75 | ASC/DESC 混合方向主键 |
| BLOB前缀索引 | case_76 | BLOB 前10字节索引 |
| TEXT前缀索引 | case_77 | TEXT 前20字符索引 |
| 多列唯一NULL | case_78 | 唯一约束允许NULL |
| SERIAL默认值 | case_79 | SERIAL 别名 |
| ON UPDATE时间戳 | case_80 | datetime ON UPDATE CURRENT_TIMESTAMP |
第三类:边界语法与MySQL 5.7/8.0特性(case_81 ~ case_120)
SRID、长标识符、高精度数值、多值索引、窗口函数、JSON_TABLE、锁相关语法……这些都是实践中容易踩坑的地方。
| 测试范围 | 表案例 | 覆盖内容 |
|---|---|---|
| 空间SRID | case_81 | geometry 带 SRID |
| 宽表 | case_82 | 10列相似列 |
| 长标识符 | case_83 | 64字符最大长度列名 |
| 保留字引用 | case_84 | select/update/delete/insert 保留字 |
| 高精度数值 | case_85 | decimal(65,30) 最大精度 |
| 时间类型混合 | case_86 | 多种时间类型混合 |
| 文本二进制混合 | case_87 | text/blob 混合 |
| 数值边界 | case_88 | 最大/最小数值 |
| 建表方式 | case_89 | LIKE/AS/ENGINE 多种建表方式 |
| 多值索引 | case_90 | MySQL 8.0 多值索引 |
| 窗口函数 | case_91 | ROW_NUMBER/RANK 窗口函数 |
| JSON_TABLE | case_92 | MySQL 8.0 JSON_TABLE |
| 锁语法 | case_93 | SELECT ... FOR UPDATE |
| CTE | case_94 | WITH 子句 |
| 递归CTE | case_95 | 递归查询 |
| LIST COLUMNS分区 | case_96 | PARTITION BY LIST COLUMNS |
| RANGE COLUMNS分区 | case_97 | PARTITION BY RANGE COLUMNS |
| KEY分区 | case_98 | PARTITION BY KEY |
| LINEAR HASH分区 | case_99 | PARTITION BY LINEAR HASH |
| 优化器提示 | case_100 | FORCE INDEX/USE INDEX |
第四类:业务化建模样例(case_121 ~ case_155)
从电商、CMS、财务、社交到医疗、酒店、餐厅,这些测试表模拟了真实业务场景下的建表逻辑。
| 业务场景 | 表案例 | 说明 |
|---|---|---|
| 电商系统 | case_121~130 | 订单、商品、用户、购物车、支付 |
| CMS系统 | case_131~135 | 文章、分类、标签、评论 |
| 财务系统 | case_136~140 | 账户、交易、报表 |
| 社交网络 | case_141~145 | 用户、好友、动态、点赞 |
| 日志系统 | case_146~148 | 访问日志、错误日志 |
| 医疗系统 | case_149~150 | 患者、病历、处方 |
| 酒店管理 | case_151~152 | 客房、预订、入住 |
| 餐厅系统 | case_153~155 | 菜品、订单、评价 |
第五类:新增综合增强场景(case_156 ~ case_167)
复合外键、JSON生成列、时间类型组合、文本二进制混合、数值边界、建表方式专项——专门用来兜住那些容易被忽略的角落。
| 测试范围 | 表案例 | 覆盖内容 |
|---|---|---|
| 复合外键 | case_156 | 多列复合外键约束 |
| JSON生成列 | case_157 | JSON 字段生成列 |
| 时间类型组合 | case_158 | datetime(6)/timestamp(6) 组合 |
| 文本二进制混合 | case_159 | text/blob/varbinary 混合 |
| 数值边界 | case_160 | decimal/numeric 边界值 |
| 建表方式专项 | case_161~167 | LIKE/AS/ENGINE/CHARSET 组合测试 |
二、分区表专项测试:4种分区策略全覆盖
分区表的迁移向来是重灾区。create_comments_partition_table.sql专门拿来做这件事。
case_169_merge:RANGE分区(单分区)
基础RANGE分区示例,只有一个分区,主键必须包含分区键issue_id,存储引擎指定为InnoDB。适用于按整数范围做简单数据划分的场景。
test_partition_170_range_int:RANGE分区(多分区)
经典多分区模式,包含5个分区,范围从1000到MAXVALUE,主键包含分区键。适用于连续整数范围的均匀数据分布——时间序列数据、ID范围分片,基本都是这个套路。
test_partition_172_list_int:LIST分区
按离散值列表划分,分区键是状态字段。p0包含0,p1包含1,p2包含2和3。主键必须包含status。需要注意的是,LIST分区不支持DEFAULT分区,插入不在列表中的值会直接报错。
test_partition_173_range_multi:RANGE分区(非均匀分布)
分区范围呈指数增长——1000、5000、10000、50000、MAXVALUE。这种非均匀策略适合数据分布不均衡的场景,早期范围小后期范围大,同时包含TEXT字段来测试大字段在分区表中的兼容性。
三、唯一键测试:6种唯一约束场景
create_unique_key_table.sql专门验证唯一键约束在迁移时是否出问题。
| 类型 | 说明 | 测试表 |
|---|---|---|
| 普通表 | 不带唯一索引 | mpp_case_normal |
| 单列唯一索引 | 约束单个字段唯一性 | mpp_case_unique_single |
| 多列唯一索引 | 约束多字段组合唯一性 | mpp_case_unique_multi |
| 普通索引 | 不应触发分布式约束 | mpp_case_non_unique_idx |
| 主键+唯一索引混合 | 验证唯一索引列是否进入分布键 | mpp_case_pk_plus_unique |
| 多个唯一索引 | 验证分布键在多唯一约束场景下的调整 | mpp_case_multi_unique_paths |
| 宽表复合索引 | 验证普通复合索引不会触发分布键 | mpp_case_non_unique_composite |
迁移到PostgreSQL或Greenplum时,有几个注意事项:UNIQUE INDEX对应CREATE UNIQUE INDEX;分区表唯一键必须包含分区键;分布式表要留意数据分布策略;NULL值处理方面,MySQL和PostgreSQL都允许多个NULL,这块倒是相通的。
四、索引全覆盖:5大类索引测试
create_index.sql覆盖了你能想到的大部分索引类型。
基础索引类型包括单列索引、复合索引、前缀索引。复合索引支持最左前缀匹配,前缀索引可以减少索引大小——这些在迁移时都有对应的处理逻辑。
特殊类型索引涵盖全文索引(FULLTEXT,支持自然语言和布尔搜索)、空间索引(SPATIAL,地理空间数据查询)和函数索引(基于表达式创建)。
分区表索引有RANGE、LIST、HASH三种,外加子分区索引。
存储引擎相关索引则区分InnoDB(聚簇索引,支持事务)、MyISAM(非聚簇索引)和MEMORY(内存表索引,数据易失)。
特殊场景索引覆盖了压缩表索引、不同字符集和排序规则的索引、生成列索引等边角情况。
五、42个测试视图:5个复杂度等级
create_view.sql定义了42个测试视图,按复杂度分成五个等级:简单(单表查询,5个)、中等(多表连接,3个)、复杂(子查询和聚合函数,3个)、高级(窗口函数和JSON操作,6个),以及MySQL 8.0特高级视图(20个)。
从简单的view_case01_simple_integers到综合了CTE、JSON_TABLE、正则表达式、GIS空间函数的view_case41_mysql8_ultimate,基本覆盖了视图迁移可能遇到的所有语法场景。
六、110个存储函数:最多涉及10表关联
create_function.sql定义了110个复杂的存储函数。每个函数2到30行代码,覆盖MySQL存储过程的核心语法,有的涉及多达10张表的关联查询。
前100个函数(func_001 ~ func_100)侧重语法转换测试,从分区表、枚举类型、JSON操作到全文检索、空间数据,每个函数都有明确的测试目标。后10个(func_101 ~ func_110)则偏向业务场景,比如复合主外键聚合计算、JSON字段提取、时间类型格式化、高精度数值聚合——这些都是从实际项目中提炼出来的需求。
七、CI流水线:10种数据库版本组合的自动化测试
有测试用例还不够。关键是——每次代码提交,怎么确保不会破坏迁移功能?
MySQL2PG的GitHub Actions CI流水线采用了10种数据库版本组合,按顺序执行,避免资源竞争:MySQL 5.7到8.0,分别对应PostgreSQL的12、14、16、17、18版本。
每个Job的流程都一样:启动MySQL容器,启动PostgreSQL容器,等待两个数据库就绪,然后执行SQL脚本初始化测试数据——167张表、每表10行数据、数百个索引、42个视图、110个函数、用户和权限、分区表注释,全套走一遍。接着生成配置文件,编译运行MySQL2PG,最后上传转换日志。
这套流水线确保每次提交都能验证:167张表结构转换、1670行数据同步、数百个索引转换、42个视图语法、110个函数映射、用户权限、分区表处理、唯一键兼容性——在10种MySQL × PostgreSQL版本组合下一一通过。
八、转换能力全景图
把MySQL2PG的转换能力归纳起来,大致可以分为四个维度。
表结构转换(DDL):AUTO_INCREMENT变SERIAL或BIGSERIAL,TINYINT(1)变BOOLEAN,INT和INTEGER保持对应,FLOAT变REAL,DOUBLE变DOUBLE PRECISION,DATETIME变TIMESTAMP,VARCHAR不变,TEXT和LONGTEXT统一变TEXT,BLOB和LONGBLOB变BYTEA,JSON保持JSON,ENUM和SET转为VARCHAR(255),DECIMAL保持,BIT保持,YEAR转INTEGER,UNSIGNED则直接移除改为有符号类型。
视图函数转换(50+函数映射):IFNULL变COALESCE,IF转CASE WHEN,GROUP_CONCAT变string_agg,CONCAT改用||操作符,NOW变CURRENT_TIMESTAMP,DATE_FORMAT变to_char,STR_TO_DATE变to_date,UNIX_TIMESTAMP变extract(epoch from ...),JSON函数群对应到jsonb的各种操作——json_build_object、jsonb_set、jsonb_array_length,等等。
索引转换:主键、唯一索引、普通索引一一对应。全文索引转为tsvector配合GIN索引,空间索引依赖PostGIS扩展,前缀索引用表达式索引模拟,函数索引同样转为表达式索引。
分区表处理:RANGE和LIST分区转为PostgreSQL的声明式分区,HASH也对应声明式,KEY分区转为HASH,LINEAR HASH转为普通HASH,子分区转为复合分区,LIST COLUMNS和RANGE COLUMNS分别转化为LIST和RANGE。
九、数据验证:确保一行都不丢
迁移完成后,工具会自动执行数据验证:MySQL行数和PostgreSQL行数逐一对比。只要有一张表的行数对不上,CI流水线就会直接亮红灯,阻止有问题的代码合并。
十、三层测试体系:确保每一次迁移都能成功
有了全面的测试用例,代码本身的稳定性如何保障?MySQL2PG建立了一套三层测试体系。
第一层:单元测试。覆盖核心转换逻辑的每个细节:配置解析、视图函数转换、函数语法映射、数据同步、DDL转换、报告生成、PostgreSQL连接管理。以视图函数转换测试为例,有88个测试用例,验证IFNULL、ROUND、MOD、DATE_FORMAT、JSON函数群的转换是否正确。当前代码覆盖率达到98%。
第二层:集成测试。在真实的MySQL和PostgreSQL环境中,验证端到端迁移流程。84个测试用例覆盖连通性、DDL转换、数据同步、视图转换、索引转换、函数转换、用户权限、运行选项、边界场景和错误处理。每个用例都验证退出码、日志、表结构、数据行数、视图和函数的可用性、索引完整性,以及权限配置。
第三层:CI流水线自动化测试。每次代码提交自动触发,10种数据库版本组合。配备了Race Detector检测并发数据竞争,持续生成覆盖率报告(88%+),有编译检查和静态分析。分支保护机制确保main分支必须有CI通过才能合并,每次发布都经过完整测试流程。
总结下来,这套体系的核心逻辑就四句话:
第一,全覆盖的测试用例——167张表、42个视图、110个函数、4种分区策略、6种唯一键场景、5大类索引,把从基础语法到业务场景的迁移需求全部兜住。
第二,三层测试防护——200+单元测试(98%覆盖率)、84个集成测试、10种CI版本组合,确保代码稳定可靠。
第三,自动化的CI流水线——每次提交自动验证,失败了就不让合并。
第四,严格的数据验证——迁移前后行数对比,数据一致性不放松。
数据库迁移不是赌运气。它靠的是体系化的测试、自动化的流程和严格的代码质量保障。MySQL2PG在这个方向上,提供了一个相当扎实的参考样本。
来源:互联网
本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。