1. 迁移这件事,难点不在"搬数据",而在"换方言"
接手过 MySQL 迁达梦数据库任务的同行应该都有同感:真正折磨人的不是数据搬不过去,而是搬完之后业务系统跑起来,暴风骤雨一般抛出来的 SQL 语法问题。上个月我刚带完一个会员系统的迁移项目,从 MySQL 5.7 迁到达梦(DM),整整一周时间里有三天都在和各种方言差异搏斗。今天把这次踩坑的过程、改写方案和迁移链路整理出来,给后面要干同样活的人留一份参考。
先建立一个基本认知:达梦虽然是国产数据库,但它的 SQL 方言整体上更接近 Oracle 体系,而不是 MySQL。所以你在 MySQL 里习以为常的写法——反引号、AUTO_INCREMENT、LIMIT 分页、IFNULL、GROUP_CONCAT、ON DUPLICATE KEY UPDATE——到了达梦这边,几乎每一类都要过一遍"翻译"的工序。一套完整的迁移方案通常由三块组成:库表结构迁移、存量数据迁移、业务 SQL 改造。前两块靠工具批量处理能完成大半,第三块才是决定项目成败的关键,因为业务代码里散落着大量日常 SQL、动态 SQL、存储过程,这些是逃不掉的返工项。
如果抱着"导出导入一把梭"的心态开工,基本都会在兼容性测试阶段被教做人。我见过最典型的情况是:迁移工具确实把几百张表的 DDL 都转成功了,数据行数也对得上,但一到联调,登录接口先报语法错误,报表接口再报函数不存在,后台任务里的存储过程直接编译失败。所以这篇文章我会把语法差异按类别拆开讲,再给一条完整可执行的迁移执行链路。
2. 动手前的兼容性体检:类型、建表语句、字符集
2.1 数据类型对照与映射
迁移之前,我建议先把源库的表结构全部导出,对照一份数据类型映射表逐项过一遍。不要相信"看起来差不多"这种判断,数据类型映射错了,后患往往在数据导入阶段才暴露。
| MySQL 类型 | 达梦(DM) | 迁移注意事项 |
|---|---|---|
| TINYINT / TINYINT(1) | TINYINT / BIT | TINYINT(1) 在 MySQL 里常被当布尔用,迁到达梦后 JDBC 读取类型会变化,应用代码要做适配 |
| INT / INTEGER | INT | 直接迁移 |
| BIGINT | BIGINT | 直接迁移 |
| DECIMAL(M,D) | DECIMAL(M,D) | 直接迁移,注意精度设置别被工具悄悄改掉 |
| VARCHAR(n) | VARCHAR(n) | 重点确认达梦的 LENGTH_IN_CHAR 参数,默认按字节算,中文场景容易发生"我以为存 50 个字,实际只能存 50 字节"的溢出 |
| DATETIME | DATETIME / TIMESTAMP | 直接迁移,注意列上的默认值 CURRENT_TIMESTAMP 需要改写 |
| TEXT / LONGTEXT | TEXT / CLOB | 直迁后 JDBC 读取接口会变,ORM 映射可能要调整 |
| BLOB | BLOB | 直接迁移 |
| ENUM / SET | 不支持 | 改为 VARCHAR + CHECK 约束 |
| UNSIGNED | 不支持 | 改用 BIGINT 或加 CHECK 约束做取值范围控制 |
| JSON | 不支持(或转文本) | 建议转 CLOB 或 VARCHAR,应用侧自行做序列化/反序列化 |
这里最容易被忽视的还是 VARCHAR 的长度单位问题。MySQL 的 VARCHAR(50) 是按 50 个字符计算的,不管中文英文都算一个字符;达梦在默认配置下按字节计算,一个中文占 2~3 字节,字段存到一半就报"超出长度"的错。迁移前先把达梦实例的 LENGTH_IN_CHAR 参数确认并统一,要么把参数调成按字符运算,要么在 DDL 转换时人工把长度乘以 3,二选一,千万别两边标准混着用。
2.2 建表语句先说清楚"方言词"
MySQL 的建表语句里有一堆达梦不认识的关键字,最典型的是 ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin。迁移工具一般会把这类语法自动剥掉,但如果你是用 mysqldump 导出的脚本再手工灌入达梦,就需要先把这些尾巴清理干净,否则直接把 SQL 文件拿到达梦的客户端里执行,报错是必然的。
还有一种容易被忽略的情况是列定义里的 COMMENT。MySQL 支持行内注释id INT COMMENT '主键',达梦也支持类似的语义,但不同版本对 COMMENT 写法的兼容程度不一样。我的经验是结构迁移阶段统一用达梦迁移工具去转换 DDL,不要手工一条条改;工具转换完再人工 review 一遍,重点看字符串长度、默认值、自增列这三类改对了没有。
2.3 字符集与长度单位的坑
字符集是另一个容易埋雷的地方。源库 MySQL 大概率是 utf8mb4,迁到达梦后如果目标库字符集没有对齐,导入的存量数据不会立刻报错,但查询出来就是一串乱码,或者某条中文数据在写入时就报"字符集不支持"。迁移前把源端、目标端、客户端工具、JDBC 连接串四处字符集都检查一遍,尽量保持 utf8 系列统一。用 JDBC 连接时,达梦的连接串里也有字符集相关的参数,配置不当会导致客户端写入的数据在库表里变了样。这个问题如果不在第一批数据导入时就发现,后面排查成本会翻倍。
3. 实战中踩过一遍的 SQL 语法差异清单
这一节是全文的核心,我把这次迁移里实际遇到、且具有普遍性的 SQL 语法问题按类别列出来,每一种都附上改造前后的写法。这些案例的共性在于:它们都是业务代码里最高频的写法,不存在"遇不到"的可能。
3.1 自增列:AUTO_INCREMENT → IDENTITY 还是序列
MySQL 里最普通的自增主键写法:
CREATE TABLE t_user ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) );到达梦这边,最直接的做法是改用 IDENTITY 自增列:
CREATE TABLE t_user ( id INT IDENTITY(1,1) NOT NULL PRIMARY KEY, name VARCHAR(50) );语法上只差一个词,但背后的行为有差异:MySQL 的 AUTO_INCREMENT 对显式插入主键值的容忍度高,DBA 手工导数据时经常带 id 值灌入;达梦的 IDENTITY 列在部分模式下不允许显式插入值,批量导入历史数据时就要先把 IDENTITY 暂时关掉,导入完成再打开。更稳妥的替代方案是使用序列(SEQ),建序列后通过触发器或应用层取 NEXTVAL 填充主键。业务系统如果对外依赖LAST_INSERT_ID()获取刚插入的主键,迁移后要改成查询序列的CURRVAL,或者用达梦提供的等价函数获取当前 IDENTITY 值。这一项建议在需求调研阶段就问清楚:业务代码里有没有依赖自增主键回填的逻辑,有的话改造工作量要提前算进去。
3.2 分页查询:LIMIT 不能闭眼用
MySQL 的分页几乎都是 LIMIT 写法:
SELECT * FROM t_user ORDER BY id LIMIT 10, 20;达梦的 SQL 方言里其实有对 LIMIT 的兼容支持,简单场景下能跑通。但我在实际项目里发现,一旦 SQL 里同时出现 ORDER BY 和子查询嵌套,LIMIT 的行为就跟 MySQL 不完全一致,有的版本还会直接报语法错误。为了减少不可控因素,我们团队最后统一改成了基于 ROWNUM 的三层嵌套写法:
SELECT * FROM ( SELECT tt.*, ROWNUM rn FROM ( SELECT id, name FROM t_user ORDER BY id ) tt WHERE ROWNUM <= 30 ) WHERE rn > 10;这个写法的原理是:最内层先做排序,中间层在排序结果上生成行号,最外层再按行号范围截取。只要排序字段是确定的,分页结果就是稳定的。如果项目里用了 MyBatis 这类 ORM 框架,分页插件本身有方言配置,把方言改成 dm 之后,插件会自动生成适配达梦的分页语句,但这种依赖框架的分页依然建议在联调阶段挑几个典型接口实测,因为不同版本插件对达梦的支持度差别不小。
3.3 字符串和日期函数全家桶改造
函数差异是语法问题里数量最多的一类。MySQL 里天天用的 IFNULL,到达梦里推荐改成 NVL 或 COALESCE:
-- MySQL SELECT IFNULL(address, '未填写') FROM t_user; -- 达梦 SELECT NVL(address, '未填写') FROM t_user;日期格式化函数的差异最坑,因为格式模板的写法完全不同。MySQL 用 %Y-%m-%d %H:%i:%s,达梦沿用的是 Oracle 风格的 YYYY-MM-DD HH24:MI:SS:
-- MySQL SELECT DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s') FROM t_order; -- 达梦 SELECT TO_CHAR(create_time, 'YYYY-MM-DD HH24:MI:SS') FROM t_order;对应的字符串转日期函数也一并改掉,STR_TO_DATE 换成 TO_DATE,NOW() 和 SYSDATE 在大多数场景等价,但为了统一风格,我们项目里规定一律写 SYSDATE。还有一个小细节:MySQL 允许SELECT 1这种不带 FROM 的语句,达梦在某些模式下要求必须有表,碰上报错就补一个FROM DUAL,这也是迁移后联调时特别容易遇到的第一波小问题。
3.4 聚合拼接:GROUP_CONCAT 换成 LISTAGG
MySQL 把多行拼成一行用的是 GROUP_CONCAT:
SELECT dept_id, GROUP_CONCAT(user_name ORDER BY user_id SEPARATOR ',') FROM t_user GROUP BY dept_id;达梦不认 GROUP_CONCAT,需要改成 LISTAGG:
SELECT dept_id, LISTAGG(user_name, ',') WITHIN GROUP (ORDER BY user_id) FROM t_user GROUP BY dept_id;LISTAGG 的写法初看有点反直觉,WITHIN GROUP 里放排序规则,第二参数是分隔符。如果拼接结果很长,还要注意达梦对 LISTAGG 返回长度的限制,超出长度的会报错或截断,MySQL 里没这个顾虑,所以迁移后要复查一下是否有长文本聚合拼接的报表 SQL。
3.5 插入冲突处理:ON DUPLICATE KEY UPDATE 的替代
MySQL 的INSERT ... ON DUPLICATE KEY UPDATE在业务代码里很常见,主键或唯一键冲突时就更新,否则插入。达梦没有这个原生语法,最常见的替代方案是 MERGE:
-- MySQL INSERT INTO t_user (id, name) VALUES (1001, '张三') ON DUPLICATE KEY UPDATE name = VALUES(name); -- 达梦 MERGE INTO t_user t USING (SELECT 1001 AS id, '张三' AS name FROM DUAL) s ON (t.id = s.id) WHEN MATCHED THEN UPDATE SET t.name = s.name WHEN NOT MATCHED THEN INSERT (id, name) VALUES (s.id, s.name);还有INSERT IGNORE这种忽略冲突的写法,达梦也不支持。要么改成先按主键查一遍再决定插不插,要么写一个带异常处理的存储过程,在唯一键冲突的异常里吞掉错误。我的建议是:数据量小且并发不高的场景,直接用 NOT EXISTS 判断;数据量大、高频写入的场景,用 MERGE 更稳。
3.6 标识符大小写与引号规则
这个坑很有意思,表面上看不是语法问题,但实际报错率极高。MySQL 在 Linux 下默认区分表名大小写,开发人员习惯了反引号包裹标识符。达梦的规则是:未加引号的标识符统一转换成大写存储,加了双引号的标识符才保留原始大小写。
举个例子,MySQL 里建表叫 UserInfo,SQL 里写select * from UserInfo没问题;到达梦后,如果建表语句经过工具转换变成UserInfo被加了双引号存储,那应用代码里不带引号的select * from userinfo就会被达梦转成大写的 USERINFO,再去匹配带引号的 UserInfo,直接报"表或视图不存在"。我给的统一处理策略是:新库表名、字段名一律用小写,SQL 里不加引号,让达梦自然转大写匹配;已经存在的驼峰命名对象,要么在 DDL 阶段全部改成小写,要么业务 SQL 里必须严格加双引号。最忌讳的是两种风格混用,排查起来特别痛苦。
3.7 存储过程:从 MySQL 风格到 PL/SQL 风格
存储过程是语法改造里最重的一块。MySQL 的存储过程风格和达梦差别很大,变量声明、参数模式、游标写法都不一样,工具通常只能转出一个框架,里面的过程体还是要人工重写。
MySQL 的典型写法:
DELIMITER $$ CREATE PROCEDURE p_test(IN x INT) BEGIN DECLARE y INT DEFAULT 0; SET y = x + 1; SELECT y; END$$ DELIMITER ;达梦的写法更接近 Oracle 的 PL/SQL:
CREATE OR REPLACE PROCEDURE p_test(x IN INT) AS y INT := 0; BEGIN y := x + 1; PRINT(y); END;改起来有几个要点:参数用 IN/OUT 显式声明;赋值从 SET 改成 :=;SELECT 直接输出结果集的写法在存储过程里受限,要结合结果集变量或游标处理。如果一个业务系统里有几十个存储过程,这块改造必须单独排期,不能混在普通 SQL 改造里一起估工作量。
4. 迁移执行链路:工具批量搬,人工精修
4.1 用达梦迁移工具跑通的流程
达梦官方提供的数据迁移工具(DTS)是迁移的第一步,我这次项目也是用它做主力。整体流程大致是:新建迁移工程,配置源端 MySQL 连接和目标端达梦连接,选择要迁移的库和对象,先做对象迁移(表结构、视图、序列等),再做数据迁移。工具会自动把 MySQL 的 DDL 转换成达梦语法,并给出转换报告。
执行时的几个实用注意点:对象迁移阶段先勾掉触发器、存储过程这类复杂对象,让它们单独处理,不要混在表结构里一起迁,否则一个对象失败会拖累整个批次;数据迁移阶段,大表建议拆成多个批次迁,单批次事务不要太大,否则内存和回滚段都扛不住;遇到外键约束,先迁父表再迁子表,或者先禁用约束,数据到位后再启用并校验一致性。工具跑完之后,用达梦的客户端连上去抽查几张表的行数和字段值,确认生成的对象名称、字段大小写是否符合预期。
4.2 工具搞不定的场景怎么兜底
工具不是万能的,它转不了三类东西:第一类是带业务逻辑复杂嵌套的存储过程,第二类是动态拼接的 SQL,第三类是特殊数据类型(比如 JSON 字段)的内容转换。这三类必须人工兜底。
存储过程在前面已经说过,动态 SQL 的问题在应用代码里更隐蔽。业务系统里常有人写String sql = "select * from t where " + condition,这类 SQL 是工具扫描不到的,只能靠应用日志或 SQL 监控把实际执行的语句抓出来逐条过。数据兜底方面,如果工具迁移中途失败、数据量大又不方便重跑,可以用导出的文本格式数据配合达梦的文本加载工具(dmfldr)做批量导入,表结构已经建好的情况下,这种方式比反复重跑图形化工具更可控。
4.3 应用侧改造顺序的建议
应用侧 SQL 改造建议按这个顺序推进:先把通用基础模块(登录、用户、权限)跑通,因为这些模块关联的表和 SQL 是最常用的;然后处理报表类查询,这类 SQL 往往函数用得最多;最后处理批量任务和存储过程。改造方式上,我强烈建议给应用增加一个 SQL 日志开关,联调阶段把所有执行的 SQL 原样打印到日志文件里,之后对日志做一次全量扫描,把包含 LIMIT、IFNULL、DATE_FORMAT、GROUP_CONCAT、ON DUPLICATE KEY UPDATE 等关键词的语句全部揪出来逐条改写。这比靠人工翻代码找 SQL 高效得多,也基本不会漏。
5. 迁移完成的判断标准不是"能启动",而是"能回归"
5.1 行数与抽样校验
数据迁移完成后,第一步就是做行数核对。写一个对比脚本,把 MySQL 每张表的行数和达梦用户下表行数批量拉出来对比。这里要用行数,不要用SELECT COUNT(*)逐表跑,大表全表计数很慢,可以借助统计信息或者分片抽查。行数对上了,再对关键业务表做抽样数据核对,每张表抽几条记录,逐字段比对;文本字段容易在字符集转换时出问题,日期字段容易在格式转换时出问题,这两个类型要重点看。
5.2 应用 SQL 兼容性回归清单
回归阶段不要只测"功能能不能用",我习惯整理一张 SQL 兼容性清单,把项目里出现过的语法模式列出来,逐项勾选验证。清单包括以下几类:
- 自增列插入和主键回填是否正常
- 分页查询在列表、报表、后台管理三类场景是否都稳定
- 空值处理(NVL/COALESCE)在聚合计算中的结果是否和原库一致
- 日期函数格式化结果是否符合应用预期
- 批量插入、批量更新的写法在达梦下的行为和性能
- 连接串、驱动类、ORM 方言配置是否都切换到了达梦
- 存储过程在任务调度里的执行结果和异常输出是否正常
这张清单每验证一项就勾掉一项,最后回归报告按清单出,项目经理看了也安心。
5.3 驱动、方言与统计信息的调整
技术栈层面还有三件事要记得做:JDBC 驱动从 MySQL 的 Connector/J 替换为达梦驱动,驱动类名对应dm.jdbc.driver.DmDriver,连接串jdbc:dm://ip:5236/库名,端口 5236 是达梦默认的,不同实例可能自定义,以实际配置为准;如果用了 ORM 框架,把数据库方言配置改成 dm,分页插件不要再用 MySQL 的分页逻辑;最后别忘了对达梦里的关键表执行统计信息收集。达梦的优化器依赖统计信息生成执行计划,刚迁移进来的表统计信息是空的或不准的,不收集就跑业务 SQL,性能会很随机,有索引也可能走到全表扫描。
6. 几次迁移下来,我最想提醒的三件事
第一件事,不要把宝全押在迁移工具上。工具解决的只是"结构能建、数据能灌"这个层面,真正的语法问题藏在业务代码和存储过程里,必须安排专人做 SQL 扫描和改造,这项工作没有捷径,老老实实把日志捞出来逐条过是最稳的。
第二件事,提前定好并记录方言差异手册。这次迁移里踩过的每个坑都值得写成一条笔记,比如"MODIFY COLUMN 语法要换成 ALTER COLUMN""BOOLEAN 类型在达梦 JDBC 下的表现""空字符串和 NULL 在分组统计里的差异",团队里每个人遇到新问题就补充一条,后面再做类似迁移,照着手册改能省一半时间。
第三件事,排序稳定性和默认值差异这类看似细枝末节的问题,反而最容易在验收阶段翻车。我曾经碰到过一条统计 SQL,MySQL 里同一个分组字段的结果顺序和达梦里完全不一致,导致报表数据对不上。这类问题不会报错,只会悄悄改变结果,所以迁移前把老系统每个关键报表的基线数据截图保存下来,迁移后逐张对比,比任何口头承诺都管用。
数据库迁移这个活,说白了就是一部"方言翻译史"。工具负责把大段的稿子翻完,剩下的零碎句子、特殊词汇,还是得靠人一个字一个字地校对。把这些问题梳理清楚再动手,库是能安全搬过去的,心里也不慌。