最近在评估一个从 MySQL 迁移到 openGauss 的项目,开始看到“兼容 MySQL”的宣传,以为建个库导个数据就能跑,结果第一张建表脚本就报错。翻文档、查示例、做对照测试,踩了一堆坑之后,我整理了一份不兼容点对比表,这里把实际遇到的和查证过的差异展开说一下。这篇文章不是劝退,而是给准备做迁移的人一个清单,减少“迁移即翻车”的概率。
1. 兼容前缀背后的设计分叉:openGauss并不想成为“另一个MySQL”
1.1 openGauss的血统决定了兼容性边界
openGauss的内核脱胎于 PostgreSQL 体系,但做了大量自研改造,和 MySQL 的兼容是“方言兼容”而不是“内核同构”。MySQL 以 InnoDB 为核心,支持插件式存储引擎;openGauss 的存储、事务、日志机制延续的是 PostgreSQL 那一套,又加了自己的行存、列存、资源池化等能力。也就是说,兼容层做在语法解析、系统视图、JDBC 协议等位置,而不是把 InnoDB 的行为模仿一遍。
这个定位带来的直接后果是:常用的 SELECT、INSERT、UPDATE、DELETE 大多能跑,但一旦触碰到 MySQL 特有语法、特殊函数、隐式规则,行为立刻分叉。比如SHOW CREATE TABLE、information_schema的一系列查询,在 openGauss 里看起来有,但返回字段和 MySQL 不同,很多 DBA 脚本迁移后直接失效。
1.2 兼容模式不是万能开关
openGauss 创建数据库时可以指定兼容模式,比如DBCOMPATIBILITY='B'(B 兼容 MySQL)或'A'(A 兼容 O 厂商)。在 B 模式下,很多 MySQL 语法会被识别,例如AUTO_INCREMENT、IFNULL、DATE_FORMAT等。但“开启兼容模式”不等于“全兼容”,官方文档里明确列了大量不兼容项,而且不同版本的兼容程度不一样。
如果创建数据库时用了默认模式,再执行 MySQL 的建表脚本,大概率是语法报错。即使创建了 B 模式库,也可能只是把“语法错误”变成了“类型不匹配”或“行为不同”。所以第一个建议永远是:先确认你的 openGauss 是什么版本、目标库是不是 B 兼容库,再谈迁移。
1.3 先看结论:哪些系统适合迁移
根据我这次评估的经验,逻辑简单、以单表 CRUD 为主、没有复杂存储过程、不依赖 MySQL 特殊函数的应用,迁移成本相对可控;但牵扯到批量 UPDATE、触发器、自定义函数、复杂权限体系、特殊排序规则的,一定要逐条核对。不兼容点对比表存在的意义,不是劝你别迁,而是让你在写改造方案前把所有坑提前标出来。
2. 建表阶段就卡壳:数据类型、默认值和时间精度差异
2.1 数值类型与自增列的映射
MySQL 建表非常喜欢TINYINT(4)、INT(11)这种带显示宽度的写法。openGauss 在 B 兼容模式下有 TINYINT 类型,但 MySQL 的显示宽度会被忽略;在非 B 模式下,TINYINT 可能直接报错,需要改成SMALLINT。
更核心的是自增列差异。MySQL 用AUTO_INCREMENT,openGauss 在 B 模式下也支持AUTO_INCREMENT,但底层实现是序列(sequence)。这带来几个实际影响:
- 重置自增值的方式不同。MySQL 用
ALTER TABLE t AUTO_INCREMENT=1000;,openGauss 需要用setval(pg_get_serial_sequence('t', 'id'), 1000, false);。 - 复制表结构时,MySQL 的
CREATE TABLE t2 LIKE t1会保留自增属性,openGauss 的LIKE语法需要额外加上INCLUDING DEFAULTS,否则默认的nextval丢失。 - 事务回滚后,两者的自增 ID 都可能不回收,但 openGauss 因为基于序列,对
setval的粒度控制比 MySQL 直接改表定义更灵活。
2.2 字符串类型和默认值
MySQL 里VARCHAR(255)的 255 通常指字符数;openGauss 的VARCHAR(n)在多数场景也是字符数,这个基本兼容。真正的坑在默认值上。
MySQL 8.0 之前BLOB/TEXT不能有默认值,openGauss 的限制更宽松,但反过来,MySQL 的TIMESTAMP可以写DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,openGauss 在 B 模式下可能部分支持,但为了稳妥,我建议迁移时把ON UPDATE CURRENT_TIMESTAMP去掉,应用层手动维护updated_at。不然哪天重启、切换驱动版本,行为变了很难排查。
另外一个隐藏差异是字符集。MySQL 的表常有DEFAULT CHARSET=utf8mb4,openGauss 没有这个属性,字符集更多是集群级或库级配置。迁移时如果不处理,中文乱码或字符集校验问题会集中爆发。
2.3 日期时间与零值问题
MySQL 的DATETIME允许0000-00-00 00:00:00这个零值,老业务里甚至有人拿它当“空值”用。openGauss 严格拒绝这种零日期,导入时直接报错。这是迁移数据时最经典的一类不兼容,必须在导出前做数据清洗,把零值改成NULL或合法日期。
时区差异也要注意。MySQL 的TIMESTAMP内部存 UTC,展示时按会话时区转换;openGauss 的timestamp without time zone不关心时区,带时区类型是timestamptz。如果原来的代码依赖 MySQL 的时区转换行为,迁移后同一列存下来的时间可能在不同地区产生偏差。
2.4 一个典型建表语句的对照
拿最简单的用户表来说:
-- MySQL 写法 CREATE TABLE t_user ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(64) NOT NULL DEFAULT '', status TINYINT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;在 openGauss B 模式下,大致的等价写法是:
CREATE TABLE t_user ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(64) NOT NULL DEFAULT '', status INT NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) );看起来差别不大,但ENGINE和CHARSET必须删掉,TINYINT可能需要调整。直接把 MySQL 脚本扔过去跑,一般会卡在第一个不认识的属性上。
3. 偷偷改语法的后果:UPDATE多表、DELETE别名与LIMIT行为对比
3.1 UPDATE 多表关联的写法完全不同
MySQL 常见的多表更新是这样:
UPDATE t1 JOIN t2 ON t1.id = t2.tid SET t1.a = t2.b WHERE t2.status = 1;openGauss 走的是 PostgreSQL 风格的UPDATE ... FROM:
UPDATE t1 SET a = t2.b FROM t2 WHERE t1.id = t2.tid AND t2.status = 1;表面只是语法调整,但有个隐藏差异:MySQL 的 JOIN UPDATE 在关联到多行时,更新结果是不确定的,官方文档也提醒不要依赖这种行为;openGauss 用FROM子句时,虽然通常也会选取某一行,但行为由执行计划决定,同样存在不确定性。迁移时不能只做语法翻译,还要检查业务是否依赖“更新哪一行”的默认行为,否则线上数据会变更出不同结果。
3.2 DELETE 的别名与多表写法
MySQL 支持DELETE a FROM t_user a JOIN t_order b ON ...这样给表起别名删除。openGauss 的等价写法是:
DELETE FROM t_user a USING t_order b WHERE a.id = b.uid AND b.create_time < '2020-01-01';注意,openGauss 对 DELETE 别名的支持在不同版本有差异,B 兼容模式下可能能识别 MySQL 风格,也可能报错。如果团队里有同学习惯了 MySQL 多表删除,迁移后第一反应是“不是兼容 MySQL 吗,为什么这里不行”,这个现象非常普遍。
3.3 UPDATE/DELETE 与 LIMIT 组合
MySQL 允许UPDATE t_user SET status = 1 WHERE status = 0 LIMIT 100;,用来分批更新。openGauss 不支持 UPDATE/DELETE 带 LIMIT,需要改写:
UPDATE t_user SET status = 1 WHERE id IN ( SELECT id FROM t_user WHERE status = 0 LIMIT 100 );但这个改写有坑:子查询里的LIMIT如果没有ORDER BY,所选行不可预期;而且 openGauss 对UPDATE目标表出现在子查询中有限制,可能直接报“UPDATE/DELETE cannot refer to the target table”。需要先SELECT id到临时表,再关联更新。这个改写过程比想象中麻烦,尤其在大表分批场景下,执行计划可能完全不是预期。
3.4 SELECT 的 LIMIT 偏移、排序兼容情况
SELECT 的LIMIT 10, 20在 openGauss B 模式下能识别,也支持LIMIT 20 OFFSET 10。但要注意,两个数据库在“不写 ORDER BY 时的返回顺序”上几乎必然不同。MySQL InnoDB 按主键聚簇,openGauss 是堆表结构,物理扫描顺序不一样。分页接口如果依赖隐式顺序,迁移后可能出现重复或缺失数据。
3.5 REPLACE INTO 与 INSERT ON CONFLICT
MySQL 的REPLACE INTO很常用,openGauss B 模式部分支持,但REPLACE本质是 delete + insert,副作用很大,会触发删除/插入类触发器、更新自增 ID、对外键不友好。openGauss 更推荐INSERT ... ON CONFLICT语法,例如:
INSERT INTO t (id, name) VALUES (1, 'a') ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;MySQL 的ON DUPLICATE KEY UPDATE可以不指定冲突列,碰到任意唯一键冲突就更新;openGauss 的ON CONFLICT必须指定唯一键或唯一约束。应用层原本一句 SQL 搞定的事务逻辑,迁移后可能要改成先查后写或拆分多条 SQL。
4. 存储过程、触发器和自增列:让老应用“迁移即翻车”的重灾区
4.1 存储过程:语言和游标都不一样
MySQL 存储过程更接近 SQL Server 风格,用DELIMITER定义结束符,循环用WHILE ... DO ... END WHILE,异常处理用DECLARE CONTINUE HANDLER FOR NOT FOUND。openGauss 的主要存储过程语言是 PL/pgSQL,用BEGIN ... END、LOOP、FOR、IF ... THEN ... END IF。
一个典型差异是游标:
- MySQL:
DECLARE cur CURSOR FOR SELECT ...; OPEN cur; FETCH cur INTO var; - openGauss:可以使用
FOR row IN SELECT ... LOOP ... END LOOP,绕开显式游标;但如果要照搬 MySQL 游标,语法差异很大。
动态 SQL 也是重灾区。MySQL 用PREPARE ... FROM ... EXECUTE,openGauss 用EXECUTE IMMEDIATE或USING参数。如果原来的存储过程里动态拼了 SQL,迁移基本等于重写。我这次评估一个订单模块,光三个存储过程就花了两天时间改写,还不敢保证所有边界条件一致。
4.2 触发器:FOR EACH ROW 的默认行为
MySQL 的触发器必须写FOR EACH ROW,openGauss 同时支持行级和语句级触发器。迁移时表面上把CREATE TRIGGER改一改即可,但触发器内部的赋值语法有差异:
- MySQL:
SET NEW.col = value; - openGauss:
NEW.col := value;
还有触发器递归行为、嵌套层级上限,两边默认值不同。如果原业务对触发器有较强的依赖,迁移后一定要做并发场景下的触发器测试,否则一个简单的 UPDATE 可能触发连锁更新,甚至死锁。
4.3 自增列:AUTO_INCREMENT、SERIAL 与 IDENTITY 的体验差异
openGauss 的AUTO_INCREMENT在 B 模式底层是序列,会生成一个隐式序列。这导致:
- MySQL 用
ALTER TABLE t AUTO_INCREMENT=1000重置,openGauss 要用setval。 SHOW CREATE TABLE时,openGauss 不会像 MySQL 那样直观显示AUTO_INCREMENT=1000,而是显示DEFAULT nextval('table_id_seq')。- 用
CREATE TABLE t2 (LIKE t1 INCLUDING DEFAULTS)自增默认值才会带过来,如果漏了INCLUDING DEFAULTS,新表的自增列就变成普通 INT 了。
这些问题不会在建表时报错,而是在数据插入后才暴露,比较隐蔽。
4.4 常用函数差异:IFNULL、GROUP_CONCAT、DATE_FORMAT
函数层面是最容易批量报错的。拿几个高频函数来说:
IFNULL(a, b):MySQL 非常常用,openGauss A 模式下不存在,需要改成COALESCE(a, b);B 模式虽然支持,但从长期维护角度看,统一用COALESCE更省心。GROUP_CONCAT:MySQL 里做行转列非常方便,openGauss 对应的函数是STRING_AGG或LISTAGG,但去重语法、排序语法都不一样。DATE_FORMAT:MySQL 的%Y-%m-%d %H:%i:%s格式符在 openGauss B 模式有支持,但官方文档也提示部分格式符行为有差异;如果不小心用了 PG 风格的to_char又是一种写法。
建议迁移前用脚本扫描代码和存储过程中的这些函数,列一个替换清单,不要等运行期一个一个爆。
5. 事务隔离、MVCC和锁行为:并发下最隐蔽的不兼容
5.1 默认隔离级别与可重复读的差异
MySQL InnoDB 默认隔离级别是REPEATABLE READ;openGauss 默认是READ COMMITTED。这个差异影响非常大,如果应用在 MySQL 下依赖“事务内多次 SELECT 结果一致”,迁移到 openGauss 后必须显式设置:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;或者按照新库重写事务边界。openGauss 也支持REPEATABLE READ,但实现机制和 InnoDB 不同。InnoDB 的快照在事务第一次读时建立,openGauss 的快照机制继承 PostgreSQL 风格,对同一行并发更新的处理逻辑不同。
5.2 UPDATE 冲突与死锁差异
并发更新同一行时,MySQL InnoDB 默认等待行锁,直到innodb_lock_wait_timeout超时,然后报锁等待超时错误。openGauss 在READ COMMITTED下,UPDATE 等待行锁释放后,会重新评估条件,可能更新零行;在REPEATABLE READ下,如果发现行已被其他事务修改,可能直接报“could not serialize access due to concurrent update”。
这个行为差异很难在功能测试阶段发现,需要在压测阶段用高并发场景去验证。我这次就遇到一个订单状态更新接口,MySQL 下一直正常,openGauss 下偶发异常,排查后发现是隔离级别下的冲突处理策略不同。
5.3 锁粒度与锁等待监控
MySQL DBA 习惯用SHOW PROCESSLIST和performance_schema.data_lock_waits查锁阻塞链,openGauss 对应的是pg_locks视图和pg_stat_activity,字段命名、等待事件类型完全不同。之前的排障脚本不能直接搬。
锁表问题在 MySQL 里通常指 DML 行锁堆积,openGauss 里还要注意 DDL 的ACCESS EXCLUSIVE锁。部分ALTER TABLE操作在 MySQL 8.0 可以在线执行,openGauss 的某些 DDL 会阻塞读写。如果业务有凌晨跑 DDL 的习惯,迁移后要重新验证这些窗口是否还能用。
5.4 隐式提交与 DDL 的事务性
MySQL 的 DDL 会隐式提交,执行CREATE INDEX或ALTER TABLE后不能回滚。openGauss 支持事务性 DDL,CREATE TABLE、ALTER TABLE可以包在事务里,执行错了可以ROLLBACK。这个差异对迁移脚本的影响是:MySQL 习惯把 DDL 和 DML 分开执行,openGauss 里 DDL 和 DML 可以混在一个事务里,但这也意味着一个长事务会持有更多锁,需要更精细地控制事务边界。
6. 一张不兼容点速查表和一个理性的迁移建议
6.1 核心对比表
以下是根据我的实测和官方文档整理的核心差异,同一版本之间可能还有微调,建议以你实际安装的版本为准。
| 维度 | MySQL | openGauss | 迁移注意事项 |
|---|---|---|---|
| 默认端口 | 3306 | 5432 | 连接串、防火墙、容器映射都要改 |
| 默认隔离级别 | REPEATABLE READ | READ COMMITTED | 依赖 RR 的应用要显式设置 |
| 自增列 | AUTO_INCREMENT | B 模式支持 AUTO_INCREMENT,底层序列 | 重置方式、LIKE 复制表结构都要注意 |
| 多表 UPDATE | UPDATE t1 JOIN t2 SET ... | UPDATE t1 SET ... FROM t2 | 语法改写,且要检查多行关联的不确定性 |
| DELETE 带 LIMIT | 支持 | 不支持 | 改子查询或临时表 |
| REPLACE INTO | 支持 | 有限支持,推荐 ON CONFLICT | 重写 SQL,注意触发器副作用 |
| ON DUPLICATE KEY UPDATE | 支持 | 用 ON CONFLICT 指定冲突列 | 唯一键处理逻辑不同 |
| GROUP_CONCAT | 支持 | STRING_AGG / LISTAGG | 排序、去重语法要重写 |
| IFNULL | 支持 | A 模式用 COALESCE | 建议统一用 COALESCE |
| DATE_FORMAT | 支持 | B 模式部分支持 | 格式符有差异,需要回归验证 |
| 零日期 | 支持 0000-00-00 | 不支持 | 导入前必须清洗 |
| 表存储引擎 | ENGINE=InnoDB 等 | 没有 MySQL 插件式引擎 | 建表脚本要去掉 ENGINE 属性 |
| 字符集 | utf8mb4 等 | 数据库/集群级配置 | 注意乱码和排序规则 |
| 存储过程 | MySQL 风格 | PL/pgSQL 为主 | 工作量最大的改写点 |
| 触发器 | FOR EACH ROW 必填 | 行级/语句级 | 赋值语法和递归行为不同 |
| 事务性 DDL | 隐式提交 | 支持事务内回滚 | 长事务和锁窗口重新评估 |
| 备份工具 | mysqldump | gs_dump | 参数、输出格式完全不同 |
| JDBC 驱动 | mysql-connector-j | openGauss JDBC | 连接 URL、SSL 参数名不同 |
6.2 迁移前必须做的几件事
我这次的实操步骤大致如下,供参考:
- 搭一个 openGauss 实例,创建 B 兼容模式数据库,把原 MySQL 建表脚本整体导入一遍,记录所有报错。
- 扫描应用代码和 SQL 脚本,找出
LIMIT在 UPDATE/DELETE 中使用、REPLACE INTO、ON DUPLICATE KEY UPDATE、GROUP_CONCAT、DATE_FORMAT、IFNULL等关键词,单独整理成改造清单。 - 导出数据前先做数据校验,重点看零日期、字符集、超长字符串。清洗逻辑可以在导出 SQL 里做,也可以在同步工具里做。
- 应用层改造连接串、驱动、JDBC 参数。openGauss 的连接串格式和 SSL 参数和 MySQL 不一样,像是
useSSL、sslmode这类参数直接套用会报错或产生误导。 - 做并发压测,重点观察死锁、锁等待、隔离级别冲突。如果原先用了
REPEATABLE READ,压测时一定要在READ COMMITTED和REPEATABLE READ两种模式下各跑一遍。 - 灰度切换前做增量同步和双写对比,不要直接全量替换。
6.3 我的建议
不用因为这份不兼容清单就觉得 openGauss 很弱。相反,它在高可用、资源池化、某些分析场景上有自己的优势。但“迁移”不是简单的数据搬家,而是一次完整的技术栈切换。最稳妥的做法是提前建一个兼容性测试环境,把自己项目的建表脚本、存储过程、核心 SQL 全部跑一遍,把报错和异常行为记录下来,再决定改造工作量。
这份对比表只是起点,实际项目里一定还会遇到文档里没写到的差异。迁移团队里最好有一个人专门负责整理“我们项目自己的不兼容点清单”,这个清单比任何官方兼容性说明都更能指导后续的落地改造。我个人的体会是,把兼容模式建对、把自增列和日期类型提前清洗干净,就能避免一半以上的迁移问题。