做数据库迁移这件事,最怕的不是数据搬不过去,而是一个系统搬过去了,SQL 却集体卡壳。mysql 迁移达梦数据库(DM)的场景,最近一两年在国产化替代的浪潮里越来越常见,但很多人第一步就走错了:以为达梦的名字里带个 MySQL 兼容,就能把原来的 SQL 原封不动地搬过去。我和团队刚把一个运行五年的订单查询系统完整迁到了达梦,期间遇到的 SQL 语法问题五花八门,排查过程也踩了不少坑。这篇就把我们遇到的语法兼容问题、排查链路以及最终落地的一套迁移方案全部梳理清楚,给后面接手这类迁移项目的人一个可以直接参考的路线图。
如果你正在做或准备做 MySQL 到达梦的迁移,这篇文章适合你。不管你是后端开发、DBA,还是架构师,里面涉及的语法差异、报错排查思路和迁移拆解顺序,都是我们在真实项目里验证过的东西。下面先讲迁移前必须做对的三件事,再逐个拆语法问题,最后落到完整的迁移方案和上线后的运维注意事项。
1. 迁移前先盘底:兼容模式、SQL语料、路线图
1.1 达梦兼容模式的第一课:没有百分百兼容
达梦数据库在初始化实例时提供了兼容模式选项,包括兼容 MySQL、兼容 Oracle 等。很多人听到这里就放心了——既然有 MySQL 兼容模式,那语法问题不就解决了吗?这个想法我们一开始也有,后来发现太天真。
达梦的兼容模式解决的是“主流语法”层面的兼容,比如支持LIMIT分页、支持AUTO_INCREMENT这种常见写法,但到了边缘语法、特殊函数、隐式类型转换这些犄角旮旯,它依然是达梦自己的行为逻辑。更关键的是,达梦本质上更贴近 Oracle 的体系,很多我们熟悉的 MySQL 特性在达梦里压根没有对等实现,比如ENUM类型、ON DUPLICATE KEY UPDATE、GROUP_CONCAT这类。
提示:不要因为在初始化时选了 MySQL 兼容模式就觉得万事大吉。兼容模式只能帮你减少踩坑概率,不能保证存量 SQL 全通过。我们在项目里是先把兼容模式选好,再靠后续的 SQL 改写和回归测试来覆盖剩余的差异。
1.2 存量SQL盘点的三个源头
迁移之前,先把系统里所有 SQL 摸清楚,这件事比想象中费力,但绝对值得做。我们的盘点发现,存量 SQL 主要来源于三个地方,每个地方的处理策略都不一样。
第一个是应用代码里的手工 SQL。这类集中在 MyBatis XML 文件、JPA 的 Native Query、以及各种灵活拼接的动态 SQL 中。数量多,但也是最需要人工逐条审查的部分。第二个是 ORM 框架自动生成的 SQL。MyBatis-Plus 的单表 CRUD、分页插件生成的带LIMIT的查询,这类 SQL 是程序生成的,改动成本低,但需要给框架配置正确的数据库方言。第三个是跑批脚本和报表 SQL,比如定时任务里的复杂统计 SQL、存储过程、视图、触发器。这类 SQL 往往是最复杂的,也是迁移失败的重灾区。
盘点完三个源头,还要给 SQL 排个优先级。我们当时按“核心链路 > 报表统计 > 管理后台 > 历史脚本”的优先级分级,核心交易链路的 SQL 优先处理、优先验证,报表 SQL 可以放到第二轮,因为这类即使出问题,影响范围也相对可控。
1.3 迁移路线总览
完整的迁移路线我们是分五步走的:结构迁移、全量数据迁移、应用层改造、功能回归、切换上线。每一步之间有明确的完成标准,避免走到一半发现前面漏了东西。
结构迁移把表结构、索引、约束先搬过去,这一步完成后就要开始做语法适配;全量数据迁移用达梦自带的迁移工具抽取数据,搬完要做行数和关键字段的校验;应用层改造是工作量最大的一步,核心是 SQL 改写和 DAO 层方言适配;功能回归要做两轮,第一轮用自动化测试跑核心接口,第二轮拉上业务方做业务场景走查;切换上线前我们刻意为达梦保留了 MySQL 的旧环境,一旦发现问题可以快速回切。
2. 高频SQL语法摩擦点:函数、类型、分页、自增
2.1 数据类型映射与默认值
数据类型映射是迁移中最容易引发“连锁反应”的一步。很多表面上迁移成功的表,实际运行时会在应用层炸掉。我们整理了常用类型的对照表和注意事项。
| MySQL 类型 | 达梦建议类型 | 迁移注意点 |
|---|---|---|
TINYINT | SMALLINT/TINYINT | TINYINT(1)在 MySQL 里常被当作布尔用,达梦 JDBC 驱动返回的是数字,ORM 里如果用了 Boolean 字段接收,会报转换错误或全部变成 true |
DATETIME | TIMESTAMP | 注意精度定义,MySQL 的DATETIME支持到秒,达梦建议显式写成TIMESTAMP(0) |
TIMESTAMP | TIMESTAMP | 达梦对默认值要求严格,DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP需要拆开写 |
ENUM | VARCHAR+CHECK约束 | 达梦没有原生 ENUM,迁移工具经常直接转成 VARCHAR,但 CHECK 约束要手工补 |
SET | VARCHAR/ 关联表 | 多值集合语义用逗号分隔字符串可以保住,但业务校验逻辑要移到应用层 |
TEXT | CLOB | CLOB 在某些操作上有隐性限制,比如GROUP BY和ORDER BY不能直接引用 CLOB |
BLOB | BLOB | 注意达梦 BLOB 的段大小与表空间配置,大对象迁移容易触顶报错 |
无符号整型UNSIGNED | 不支持 | 需要加CHECK约束代替,或者应用层保证非负 |
这里最坑的是日期类型的默认值。MySQL 允许DATETIME列设置'0000-00-00 00:00:00'这样的零值,但达梦不允许。我们迁移一个历史表时就遇到过这个情况,一列create_time在 MySQL 里存了零值,数据同步时达梦直接报错,后来只能在同步脚本里统一把零值改写为NULL或一个合法的最小时间。
2.2 自增列:AUTO_INCREMENT 和 IDENTITY 之间没有“自动平移”
MySQL 的自增列很直接:INT AUTO_INCREMENT PRIMARY KEY,插入时不指定该列,数据库自动分配连续值。达梦虽然兼容模式下也能识别AUTO_INCREMENT,但底层实现机制不同,抵达梦的推荐做法是使用IDENTITY自增列。
迁移过程中最容易出问题的不是建表,而是“数据搬过去之后,下次插入报主键冲突”。原因是:数据迁移工具把表结构和历史数据搬过去了,但自增序列的当前值没有同步过去。我们当时遇到的情况是:用户表最大 ID 是 85000,迁移完成后应用插入新记录,报主键冲突,查了下才发现自增序列从头开始计数,一下撞上了现有数据。
解决办法有两类。一类是迁移完成后手工核对自增序列的当前值,让它从“表中已有最大 ID + 1”的位置继续增长;另一类是如果业务允许,把自增列改成显式 ID + 序列生成,但这种改造影响面太大,一般不建议迁移时顺手做。
2.3 分页查询两种写法的取舍
分页查询是 MySQL 迁移到达梦的高频差异点。MySQL 里标配的LIMIT+OFFSET写法在达梦的 MySQL 兼容模式下可以跑通,但我们实际验证发现,部分场景下还是建议改写为ROWNUM方式,尤其是涉及复杂子查询和排序时。
| 写法 | 示例 | 说明 |
|---|---|---|
| MySQL 原版 | SELECT * FROM orders ORDER BY id LIMIT 10, 20 | 兼容模式下可用,但深分页性能堪忧 |
| 达梦 ROWNUM | SELECT * FROM (SELECT t.*, ROWNUM rn FROM orders t ORDER BY id) WHERE rn > 10 AND rn <= 30 | 需要嵌套一层,排序必须在内层完成 |
| 达梦 TOP | SELECT TOP 20 * FROM orders WHERE id > 10 ORDER BY id | 适合只取前 N 条的场景 |
这里给一个实用建议:迁移时把 DAO 层的分页查询统一改写为ROWNUM风格,而不是依赖兼容模式。原因有两个:一是兼容模式下执行计划不一定最优;二是如果后续达梦数据库调整了兼容模式或参数,LIMIT的行为可能会发生变化,改写为原生写法更稳定。
2.4 高频函数对照表
函数差异是面试官最喜欢问、实际项目里坑最密集的地方。下面这张表是我们从上千条 SQL 里实际总结出来的高频替换清单。
| MySQL 函数/语法 | 达梦替换方案 | 实际坑点 |
|---|---|---|
IFNULL(a, b) | COALESCE(a, b)或NVL(a, b) | 兼容模式下也许能跑,但大量使用IFNULL的 SQL 建议还是统一替换 |
NOW()/SYSDATE() | SYSDATE/CURRENT_TIMESTAMP | 注意在 SQL 中直接使用SYSDATE时不要加括号,加括号可能解析错误 |
DATE_FORMAT(d, '%Y-%m-%d') | TO_CHAR(d, 'YYYY-MM-DD') | 这是替换量最大的函数,格式化符号完全不同 |
DATE_ADD(d, INTERVAL 1 DAY) | DATEADD(d, 1, DAY) | 参数顺序不同,容易搞反 |
GROUP_CONCAT | LISTAGG | 二者对 NULL 值、去重、排序的行为都不一样,不能只改函数名 |
FIND_IN_SET(a, b) | 按业务语义改写 | 通常改写为INSTR配合逗号处理,但语义要仔细确认 |
REPLACE INTO | MERGE INTO | MERGE适合做存在则更新、不存在则插入的逻辑 |
INSERT ... ON DUPLICATE KEY UPDATE | MERGE INTO | 这是批量保存场景最常踩的坑,下文详细展开 |
正则REGEXP | REGEXP_LIKE | MySQL 的REGEXP操作符和达梦的REGEXP_LIKE函数语法细节差异不小 |
CONCAT(a, b, c) | CONCAT或 ` |
这张表建议直接保存下来,做 SQL 改写时对照使用。
3. 三个真实报错的完整排查链路
3.1 GROUP_CONCAT 改写后冒出的“无效的列名”
第一个印象深刻的坑,发生在迁移一个报表查询时。原 SQL 用GROUP_CONCAT把某个分组下的多条记录拼接成一个字段,改造时我们直接替换成了达梦的LISTAGG,逻辑上看起来没毛病,结果一执行就报“无效的列名”。
排查的过程是这样的:先把报错的 SQL 原样在达梦客户端里逐步简化,把LISTAGG部分单独拿出来跑,发现单独执行正常;再把GROUP_CONCAT原本关联的子查询加回去,又报错。最后定位到根因:LISTAGG在达梦中对 OVER 子句和分组内排序的支持与 MySQL 的GROUP_CONCAT完全不同,我们在改写时把原 SQL 里的ORDER BY子句位置照搬了,导致达梦把排序字段误判为列引用失败。
这里要特别提醒一下:GROUP_CONCAT到LISTAGG不是简单改函数名。MySQL 的GROUP_CONCAT(DISTINCT col ORDER BY col SEPARATOR ';')在达梦里要拆成LISTAGG(DISTINCT col, ';') WITHIN GROUP (ORDER BY col)的格式,DISTINCT、SEPARATOR、排序位置三者的处理都要重新检查。
3.2 ON DUPLICATE KEY UPDATE 在批量保存场景的替换
第二个坑,是应用层批量保存的接口大面积报错。原代码用的是 MyBatis 的批量插入写法,SQL 长这样:
INSERT INTO user_role (user_id, role_id, status) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE status = VALUES(status)这段 SQL 是为了已存在关系时更新状态。在 MySQL 里非常顺手,但达梦不认ON DUPLICATE KEY UPDATE,直接语法错误。我们的处理方案是改成达梦支持的MERGE INTO:
MERGE INTO user_role t USING (SELECT ? AS user_id, ? AS role_id, ? AS status) s ON (t.user_id = s.user_id AND t.role_id = s.role_id) WHEN MATCHED THEN UPDATE SET t.status = s.status WHEN NOT MATCHED THEN INSERT (user_id, role_id, status) VALUES (s.user_id, s.role_id, s.status)这个改写本身不难,但要注意三点:一是MERGE的匹配条件必须能唯一定位到记录,不然会报“结果集不是一行”之类的错误;二是应用层传入的批量数据最好分批执行,一次MERGE塞几千条数据,达梦的解析和锁开销都不小;三是VALUES()函数在达梦的 INSERT 语句中有不同语义,不要照搬 MySQL 的写法。
3.3 REGEXP 正则查询静默返回空集的陷阱
第三种情况是最隐蔽的——不报错,但查出来的结果是错的。应用里有个按关键词模糊匹配的查询,MySQL 里用的是REGEXP语法,迁移到达梦后改成REGEXP_LIKE,测试时发现同样的数据,老系统能查出来,新系统查不出来。
排查后发现是两个原因叠加:第一,MySQL 的REGEXP默认大小写不敏感,而达梦的REGEXP_LIKE对大小写敏感,匹配时给定关键词的大小写不一致就直接漏数据;第二,原 SQL 里的正则字符串包含了一些 MySQL 特有的转义写法,在达梦的正则引擎里解析结果不同,导致匹配行为变异。
这类“不报错但结果不对”的问题在迁移中是最危险的,因为常规回归测试如果只核对报错情况,根本发现不了。我们的对策是:对所有涉及字符串匹配的 SQL,准备一组边界测试数据,分别跑旧库和新库,逐条比对结果,绝不假设改写后逻辑等价。
3.4 通用排查三步法
经历了这几个坑之后,我们沉淀了一套通用排查流程,对任何 SQL 兼容性报错都管用。第一步是“最小复现”,把报错的完整 SQL 逐步简化,找到触发报错的最小子集,这个过程能快速排除无关因素;第二步是“逐段对照”,把 MySQL 原版 SQL 和达梦改写版 SQL 按逻辑段对照,标记所有函数、类型、方言差异,这一步靠的是前面的语法对照表;第三步是“结果比对”,不只比较是否报错,还要比较返回值的数量、内容和顺序,避免静默性的逻辑偏差。
提示:这套排查方法最好的落地工具是一张“SQL 改写核对表”,每一段改写过的 SQL 都在表里记录原版本、新版本、改写原因、测试结果四列。项目后期这个表就是团队的经验库,也是你写这篇文章的素材来源。
4. 迁移方案落地:从DTS工具到应用层适配
4.1 DTS迁移工具的边界与DDL修正
达梦自带的 DTS 数据迁移工具是结构迁移和数据迁移的主力,它可以连 MySQL 数据源,读取表结构并生成达梦的建表语句,同时把存量数据一并搬过去。这个工具能省不少事,但不能完全信任它的输出。
用 DTS 迁移完数据结构后,我们做的第一件事是导出一份完整的 DDL 人工审查。审查重点包括:所有ENUM类型被转成什么了、CHECK 约束是否保留、索引名是否冲突、字段注释是否丢失、字符集和排序规则是否按要求设置。当时我们就发现 DTS 把几个ENUM列转成了 VARCHAR,但没有生成对应的 CHECK 约束,应用层如果写入非法值,MySQL 时代有约束兜底,到达梦就彻底裸奔了。
数据迁移阶段,建议分两步走而不是一键全量。先迁移基础数据和历史数据,校验通过后,再把切换前积累的增量数据用工具或脚本补上。全量一把梭的结果是,一旦中途报错,定位问题要花半天,远不如分批踏实。
4.2 DAO层SQL改造清单
数据迁移完,真正耗费人力的改造在 DAO 层。我们按三种形态列了一份改造清单。
第一种是 MyBatis XML 里的手工 SQL。这类 SQL 要逐条过,优先处理报表查询和批量操作相关语句。改造时把上面那张函数对照表摆在旁边,改完一条标记一条,不要跳着改,否则容易漏。第二种是 MyBatis-Plus 这类框架自动生成的 SQL,单表 CRUD 基本不用动,但分页插件要切换到支持达梦的方言,否则生成的LIMIT语句在部分场景下还是会有问题。第三种是存储过程和触发器,这类我们给出的建议非常直接:能不迁就不迁,改用应用层逻辑实现;必须迁移的,按达梦的 PL/SQL 语法重写,而不是在 MySQL 存储过程基础上做局部替换。原因是 MySQL 存储过程与达梦在变量声明、游标、异常处理上的语法差异太大,局部替换往往越改越乱。
4.3 数据一致性校验:不只要对行数
数据搬完之后必须做一致性校验,这一步绝不能省。我们当时的校验分三层。第一层是行数校验,每张表两边各跑一个COUNT(*)对比,这个只防“少数据”;第二层是抽样字段校验,针对每张大表抽几个关键字段做SUM或MAX/MIN对比,这个能防“字段值错位”;第三层是业务维度校验,拿几条代表性的业务数据,在旧库和新库里跑相同的业务查询,人工对比结果。三层全过,迁移团队才有底气把流量切过去。
4.4 性能回归:别让报表查询比原来慢十倍
功能跑通只是第一步,性能回归才是最刺激的环节。我们遇到过一条报表 SQL 在 MySQL 上跑 2 秒,到达梦跑了几分钟的情况。原因是多方面的,但最常见的三个因素是:统计信息缺失、SQL 写法触发类型隐式转换、索引策略不适配。
达梦的优化器依赖统计信息,迁移后如果不重新收集,执行计划很可能走全表扫描。处理方式是迁移后立刻收集统计信息,并安排定期任务。索引方面,MySQL 里有效的复合索引,到达梦后由于优化器行为不同,不一定会被选用,需要重新看执行计划。类型隐式转换方面,最常见的是WHERE create_time = '2024-01-01'这种字符串和日期直接比较的写法,达梦容易不走索引,要改成TO_DATE()。
5. 上线后容易被忽略的几件事
5.1 统计信息收集与执行计划
上线踩了一轮性能坑之后,我们把统计信息收集放进了上线检查清单里。达梦提供了类似 Oracle 的包,可以手动收集某个 Schema 的统计信息。注意收集时机,也要注意别在业务高峰期跑全库收集,否则 CPU 会被打满。
上线后前两周,我们每天都会抽查几条核心 SQL 的执行计划,对比前一天的变化。因为达梦的优化器在某些复杂连接查询上可能会因为参数变化生成不同的执行计划,早发现早干预,等业务方投诉就晚了。
5.2 字符集与大小写:悄悄改坏数据的元凶
字符集问题不报错,但会造成很隐蔽的数据异常。MySQL 那边用得多的是utf8mb4,达梦初始化时设置的字符集可能是 UTF-8 也可能是 GB18030,如果两边字符集不一致,搬过去的数据轻则显示乱码,重则在排序、去重、字符串比较上全部异常。
大小写问题也要注意。MySQL 在 Windows 上表名不区分大小写,Linux 上区分;达梦数据库的标识符处理规则和 MySQL 又不一样。我们迁移时遇到过一个问题:MySQL 里的表名全是小写,迁移到达梦后,应用代码里某个查询用了驼峰写法,MySQL 下没问题,达梦下直接报“无效的表名”。所以应用层 SQL 中的表名、列名大小写,迁移前就要统一规范,最好全部使用同一种风格。
5.3 存储过程和触发器:能绕开就绕开
前面提过一次存储过程,这里再展开说。我们系统里有几个 MySQL 的存储过程,用于夜间跑批。迁移时最开始尝试直接在达梦里修改语法,改了几轮,总是有各种兼容问题。后来我们决定换一种思路:把存储过程里的逻辑改写成应用层脚本(定时任务 + Java 代码),跑批逻辑抽出来,用平台无关的方式实现。改造完之后,稳定性和可维护性反而比之前更好了。
触发器的处理类似。MySQL 和达梦的触发器语法差异同样很大,如果触发器逻辑比较简单,建议改写为应用层调用;如果触发器逻辑很复杂,说明这套设计在迁移后维护成本极高,值得趁迁移的机会借机重构。
5.4 保留一套MySQL对照环境
最后一条建议,可能听着不够“技术”,但非常管用:在整个迁移项目中,保留一套可以随时启用的 MySQL 对照环境。我们在切换前一直维持了 MySQL 旧环境的运行,达梦新环境与旧环境并行了一段时间。每次遇到“结果对不上”“性能差距大”的问题,就在两套环境上跑同一条 SQL,快速定位是数据问题、SQL 改写问题还是优化器行为差异。这套对照环境直到新系统稳定运行了一个月之后才下线。
6. 最后分享几个让我省时省力的小习惯
6.1 建立SQL改写语料库
迁移结束后我们积累了一个内部 Wiki,里面按“原 MySQL 写法、达梦改写写法、关联坑点”整理了两百多条改写记录。后面团队再遇到新问题时,第一反应不是从头排查,而是先去语料库里搜一遍,能解决掉大部分重复问题。如果你现在正处在迁移初期,建议从第一天就开始建这个语料库,每个人改完的 SQL 都往里丢一条,项目做完它就是最珍贵的知识资产。
6.2 自动化语法冒烟脚本
第二个习惯是写一个自动化语法冒烟脚本。把所有改写过的核心 SQL 汇总到一个脚本文件里,在达梦环境上批量执行一遍,执行失败或执行结果异常的自动输出告警。每次修改完 DAO 层代码或者 XML 文件,先跑一遍这个脚本,而不是等到部署后让测试同学去发现。我们的冒烟脚本后来还加了一个功能:把单条 SQL 的执行计划输出到日志里,方便快速定位性能问题。
6.3 按“交易链路优先、报表次之、脚本最后”排优先级
这是我们的第三个习惯,也是迁移排期的重要原则。迁移时先保证核心交易链路的所有 SQL 100% 验证通过,再处理报表类 SQL,最后才是各类后台脚本和一次性历史数据脚本。很多团队把精力耗在冷门脚本的完美适配上面,结果上线日被一条核心查询卡住,得不偿失。把优先级排明白,至少能保证系统即使还有少数边缘 SQL 没适配完,也不影响主业务跑起来。
迁移真不是一锤子买卖,能不能顺利切换,依赖的是前期盘底的细致程度和排查问题时一套稳定的方法论。上面这些经验和方案,是我们在真实项目里踩坑踩出来的,拿走直接用,能帮你把迁移路上最陡的几个坡提前铲平。