这两年国产化数据库替换的项目越来越多,MySQL到DM(达梦)的迁移几乎成了标配任务。老实说,我第一次接到迁移任务时也以为就是“导出表结构、灌入数据”两小时收工的事,真上手才发现从字段类型、自增列、存储过程到JDBC连接串,处处有坑。这篇文章是我做了多个迁移项目后沉淀下来的一套完整流程,覆盖迁移前的准备、工具选型、结构迁移、数据搬家、SQL方言改写、失败排查到最后的验证与适配,尽量把能踩的坑提前标出来。如果你正在处理“MySQL迁达梦”的活儿,这篇应该能让你少走几段弯路。
1. 迁移调研与准备:先别急着装工具
很多人拿到迁移任务第一件事就是打开DTS工具拖拽表,然后失败、报错、网上搜解决方案,折腾两天发现是流程顺序反了。迁移之前真正该做的是先盘清楚自己的MySQL到底长什么样,再决定达梦这边怎么初始化。
1.1 盘点源端MySQL的“家底”
这一步不涉及任何数据库迁移技术,但对后续影响巨大。我会拿个表格逐项登记,确保心里有数:
| 检查项 | 具体要确认的内容 | 为什么重要 |
|---|---|---|
| MySQL版本 | 5.7还是8.0,官方版还是云厂商RDS | 5.7和8.0在默认字符集、排序规则、SQL行为上有差异 |
| 数据规模 | 总数据量、表数量、单表最大行数、大字段总量 | 决定全量迁移还是分批次迁移,以及是否要用高速导入工具 |
| 对象清单 | 存储过程、函数、触发器、事件、视图、自定义函数 | 这些是迁移中报错最集中的区域,需要额外花时间改写 |
| 应用侧依赖 | 使用JDBC/ODBC/ORM,是否用了MySQL特有函数,SQL里有没有方言 | 迁移不只是搬数据,应用连接串、SQL语法都要跟着调整 |
建议直接用SQL统计行数和容量,类似这样:
SELECT table_schema, table_name, table_rows, ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb FROM information_schema.tables WHERE table_schema = '你的库名' ORDER BY size_mb DESC;注意table_rows是估算值,但排序足够用了。另一个容易漏掉的是MySQL库里的sql_mode,比如启用了ONLY_FULL_GROUP_BY,很多看似正常的SQL在严格模式下会失败。迁移到达梦后,达梦对分组查询的校验也有自己的逻辑,这类SQL往往是最先暴露问题的。
1.2 达梦实例的“兼容性”该怎么规划
达梦有一个COMPATIBLE_MODE参数,可以在实例初始化时设为MySQL兼容模式。很多人迁移前不考虑这个,创建库直接欧拉一套默认配置,结果建表语句各种报错。如果条件允许,建议在初始化实例时就设置兼容模式:
dminit PATH=/data/dmdata DB_NAME=DMDB INSTANCE_NAME=DMSERVER PORT_NUM=5236 COMPATIBLE_MODE=4COMPATIBLE_MODE不同版本取值含义不完全一样,一般0是Oracle兼容,1是MySQL兼容,4可能对应MySQL 8.0,你在做之前最好用dminit help确认一下手册。已经建好实例的话,也可以查看当前值:
SELECT * FROM V$PARAMETER WHERE NAME = 'COMPATIBLE_MODE';但要注意,兼容模式不是万能开关,它主要影响SQL语法解析和部分系统函数行为,解决不了所有迁移问题,后续的数据库对象还是需要人工校对。另外确认达梦版本是否为最新的稳定版,很多奇怪问题在补丁版本中已经修复,能推到新版本就不要在旧版本上死磕。
2. 迁移工具的取舍:DTS之外还有别的选择
谈到数据迁移,大家最熟悉的是达梦自带的DTS图形化工具。它确实好用,但我也见过不少项目把它当成唯一方案,结果被坑得很惨。正确的思路是了解每种工具的边界,组合使用。
2.1 达梦自带DTS工具的真实体验
DTS(Data Transfer Service)通常装在达梦数据库客户端目录下,打开后可以配置MySQL数据源,把表结构、数据直接迁移过来。优点是操作直观,能自动把MySQL字段类型映射成达梦类型,也能生成迁移报告。缺点也很明显:
- 大表迁移时容易内存占用过高,甚至连接中断。
- 存储过程、函数、触发器的迁移成功率不高,很多时候要手动改写。
- 默认映射保守,生成的字段类型不一定最优。
- 大批量对象同时迁移时,出错后定位困难。
我的经验是:小表、简单表直接用DTS没问题,但千万不要一次性勾选几十张表然后点“开始”。更稳的做法是先同步表结构,再同步数据,分两步执行。DTS的日志里会详细记录每张表的行数和耗时,一旦中间失败,优先看“日志信息”页签,通常能看到具体是哪个表因为什么原因失败。如果日志里提示“无法加载mysql驱动”,多半是DTS没找到MySQL的JDBC驱动包,去达梦安装目录的drivers/jdbc下放一份,并设置好驱动类名com.mysql.jdbc.Driver或com.mysql.cj.jdbc.Driver。
2.2 为什么推荐“半自动”的组合策略
我会把工具分成几类:结构迁移工具、数据同步工具、命令行工具。中小型表用DTS全自动;大表和复杂对象用“手工SQL + 命令行工具”半自动处理。原因很简单,工具适合批量、重复、简单流程,但碰到真正棘手的大对象时,工具的处理策略往往不是最优解。
比如一张几亿行的日志表,DTS一条一条INSERT进去会非常慢,这时候用达梦的dmfldr高速加载工具或者dmexp/dmpimp反而更有优势。所以我的迁移流程通常这么安排:
- 先用DTS同步所有小表和中等表,完成后立即校验。
- 大表单独导出成文本文件,再通过
dmfldr批量导入。 - 存储过程、函数、触发器、视图等手工迁移,不指望工具自动转。
- 最后通过SQL脚本补索引、约束、注释和触发器等附加对象。
这样做的逻辑很清楚:让工具处理它擅长的事,复杂场景人为介入,避免“一刀切”带来的返工。
3. 结构迁移的逐点对照:数据类型、约束与默认值
这部分是整个迁移中最容易被低估的环节。表面看都是CREATE TABLE,但MySQL和达梦的字段类型体系差异很大,直接照搬要么报错,要么生成性能很差的表结构。
3.1 MySQL到DM的数据类型映射表
下面是一份我整理过的常用映射参考,按经验不断修正过,可以当个速查表:
| MySQL类型 | 达梦类型 | 说明 |
|---|---|---|
| TINYINT | TINYINT / SMALLINT | 如果TINYINT(1)表示布尔值,建议迁移为SMALLINT或BIT,避免应用层取值类型混淆 |
| SMALLINT | SMALLINT | 无符号类型建议升一级到INT,因为达梦对无符号支持比较弱 |
| MEDIUMINT / INT UNSIGNED | INT / BIGINT | 无符号INT必须升为BIGINT,否则溢出 |
| BIGINT | BIGINT | 注意达梦的BIGINT范围与MySQL一致 |
| DECIMAL / NUMERIC | DECIMAL / NUMERIC | 精度、标度保持一致即可 |
| FLOAT / DOUBLE | FLOAT / DOUBLE | 注意浮点比较的误差行为,必要时改为DECIMAL |
| CHAR / VARCHAR | CHAR / VARCHAR | 注意字符长度单位,达梦默认按字符计算,和MySQL的字符语义一致,但多字节字符集下要确认 |
| TEXT / TINYTEXT / MEDIUMTEXT / LONGTEXT | TEXT / CLOB | 达梦TEXT等价于CLOB,长文本类型建议用CLOB |
| BLOB / BINARY / VARBINARY | BLOB / VARBINARY | 二进制类型也要按长度和使用方式调整 |
| DATETIME | TIMESTAMP / DATETIME | 达梦有DATETIME,也可以映射TIMESTAMP |
| TIMESTAMP | TIMESTAMP | 注意MySQLON UPDATE CURRENT_TIMESTAMP行为达梦不支持 |
| DATE | DATE | 好迁移 |
| JSON | CLOB / TEXT | 达梦有JSON类型(不同版本支持度不同),建议保守地映射为TEXT或CLOB,应用层再自己解析 |
| ENUM / SET | VARCHAR / 自定义约束 | 建议转换为VARCHAR,并在应用层维护枚举值;否则通过CHECK约束管理有限值 |
| GEOMETRY | ST_GEOMETRY | 复杂空间数据类型,除非业务明确用到,否则迁移成本高 |
映射之外还要注意字符集问题。MySQL常用utf8mb4,达梦建库时建议用UTF-8,字符集不一致会导致中文乱码、字符串比较异常,甚至在导入时报“字符串转换错误”。检查达梦库字符集可以看初始化参数CHARSET,用SELECT * FROM V$DATABASE;查看相关信息。
3.2 自增列、主键与默认值的处理
MySQL的自增列在达梦里有两种方案:IDENTITY列,或者序列加触发器。
IDENTITY列最简单,建表时可以直接写:
CREATE TABLE user_info ( id INT IDENTITY(1,1) PRIMARY KEY, name VARCHAR(64), create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );但要注意,达梦的IDENTITY列不是所有场景都能直接兼容。如果原表已经把AUTO_INCREMENT列作为复合主键的一部分,迁移时建议先建普通列,再通过序列加触发器实现自增。序列方案长这样:
CREATE SEQUENCE seq_user_id START WITH 1 INCREMENT BY 1; CREATE OR REPLACE TRIGGER trg_user_info_id BEFORE INSERT ON user_info FOR EACH ROW BEGIN IF :NEW.id IS NULL THEN SELECT seq_user_id.NEXTVAL INTO :NEW.id FROM DUAL; END IF; END;这个方案能保住现有主键值,也方便后续偶尔手工指定ID。再来看默认值,MySQL最常见的两个默认值函数CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP。达梦支持DEFAULT CURRENT_TIMESTAMP,但“行更新时自动更新时间戳”没有等价语法,需要另建触发器实现:
CREATE OR REPLACE TRIGGER trg_user_info_upd BEFORE UPDATE ON user_info FOR EACH ROW BEGIN :NEW.update_time = CURRENT_TIMESTAMP; END;如果原表里有大量这种字段,迁移前先统计一下,分批写成触发器模板,能省不少事。
4. 数据搬家的完整流水线:导出、传输、导入
结构弄完后就是数据搬迁。这个阶段最怕的是“一把梭”,几千万行的表直接跑INSERT,不仅慢,还会因为事务日志膨胀导致数据库异常。我按表规模把数据迁移分成三条路。
4.1 大表迁移的切分方案
单表超过500万行,或者整体超过2GB的,我建议不要依赖DTS的INSERT方式,而是走“文本导出 + 高速加载”。具体步骤是先在MySQL端把数据导成文本文件,再传到达梦服务器上用dmfldr批量装载。
MySQL导出文本用mysqldump的--tab参数是最省事的:
mysqldump -uroot -p --no-create-info --tab=/data/export/ --fields-terminated-by=',' --lines-terminated-by='\n' your_db your_big_table注意--tab会为每个表生成一个.txt文件和一个.sql文件,.sql里是INSERT语句,.txt是纯数据。我们只要.txt,因为接下来要喂给达梦的dmfldr。
到了达梦这边,需要手动写一个控制文件big_table.ctl,内容类似:
LOAD DATA INFILE '/data/export/your_big_table.txt' INTO TABLE your_big_table FIELDS TERMINATED BY ',' TRAILING NULLCOLS ( id, name, create_time )然后执行:
dmfldr USERID=SYSDBA/your_password@localhost:5236 CONTROL='/data/export/big_table.ctl'这个工具很能吃数据,几亿行的表也能较快跑完。但有一点要提醒:如果原MySQL的字段是NULL,而文本里用了空字符串表示,dmfldr的TRAILING NULLCOLS会让它们变成NULL,要根据业务语义仔细核对。更安全的做法是在MySQL导出时用固定的空值标识,比如把NULL统一转成\N,再在控制文件里用NULLIF (column_name=BLANKS)之类的语法处理。
4.2 导入时的批次提交与错误处理
不管用什么方式导入,批次提交都很重要。dmfldr默认会按行数分批提交,可以设置ROWS=10000指定每个批次的行数,这样一个批次失败不会影响已经提交的部分,排查问题也方便。命令里加一行:
dmfldr USERID=SYSDBA/your_password@localhost:5236 CONTROL='/data/export/big_table.ctl' ROWS=10000如果担心坏数据打断整个任务,可以加上ERRORS=1000,表示最多容忍1000行错误,超过再终止;同时用LOG='/data/export/big_table.log'指定日志路径。迁移完成后第一件事就是比对行数:
SELECT COUNT(*) FROM your_big_table;和源库的SELECT COUNT(*)对照。如果对不上,优先查dmfldr日志里的错误行,大多是字符集、数值溢出或字段含义不一致的问题。字符集不一致的典型场景:MySQL导出时是UTF-8,达梦库是GBK,干垃圾数据乱码但能进去,行数对得上可内容不对。所以dmfldr控制文件里也能指定字符集,不要偷懒省略。
5. 存储过程、函数和触发器的SQL方言改写
这部分才是迁移工作真正的深水区。表结构可以靠工具自动映射,但存储过程和函数里的SQL方言必须人工介入。MySQL的SQL风格和达梦的PL/SQL风格差异非常大,如果团队里有人之前写过Oracle,会比较有优势。
5.1 MySQL与达梦在PL/SQL上的主要差异
先看一个最典型的差异:存储过程的整体结构。
MySQL写法:
DELIMITER $$ CREATE PROCEDURE sp_get_user(IN p_id INT) BEGIN DECLARE v_name VARCHAR(64); SELECT name INTO v_name FROM user_info WHERE id = p_id; SELECT v_name; END$$ DELIMITER ;达梦写法:
CREATE OR REPLACE PROCEDURE sp_get_user(p_id INT) AS v_name VARCHAR(64); BEGIN SELECT name INTO v_name FROM user_info WHERE id = p_id; PRINT v_name; END;几个关键差异点:
- MySQL的
DELIMITER是客户端命令,达梦不需要。 - 达梦的变量声明在
AS和BEGIN之间,MySQL的DECLARE在BEGIN内。 - MySQL的
SELECT xx;直接返回结果集,达梦要用PRINT或通过OUT参数返回,或者用RETURN。 - 达梦支持
CREATE OR REPLACE,MySQL 8.0之前的版本不支持直接OR REPLACE。
还有异常处理,MySQL用DECLARE ... HANDLER FOR ...,达梦用EXCEPTION WHEN ... THEN,两者完全是两套逻辑。比如一个“捕获任意异常并输出错误码”的存储过程,MySQL写法有SQLEXCEPTION,达梦则是:
EXCEPTION WHEN OTHERS THEN PRINT SQLERRM;所以迁移存储过程不是把语法改一改就完事,而是要把整体逻辑重写一遍。
5.2 常用改写套路:游标、函数、排序
游标方面,MySQL里常用DECLARE cur CURSOR FOR ...,达梦同样支持游标,而且更贴近Oracle的FOR ... LOOP写法。比如:
FOR rec IN (SELECT id, name FROM user_info LIMIT 10) LOOP PRINT rec.id || ':' || rec.name; END LOOP;这里面的LIMIT 10要改成达梦的FETCH FIRST 10 ROWS ONLY,或者WHERE ROWNUM <= 10。PRINT rec.id || ':' || rec.name用到了||字符串连接符,达梦支持这种写法。
函数替换方面,列出几个高频等价关系,遇到直接对照改:
| MySQL | 达梦 | 说明 |
|---|---|---|
| IFNULL(a,b) | NVL(a,b) 或 IFNULL(a,b) | 达梦兼容IFNULL,但NVL更通用 |
| CONCAT(a,b) | a || b 或 CONCAT(a,b) | 推荐用||,中文环境少踩隐式转换 |
| GROUP_CONCAT(x) | LISTAGG(x, ',') | 达梦有LISTAGG聚合函数 |
| NOW() / SYSDATE() | SYSDATE / NOW() | 达梦可用SYSDATE |
| DATE_ADD / DATE_SUB | DATEADD(d, n, date) | 参数顺序不同 |
| FIND_IN_SET(str, list) | 无内置等价 | 需要改写为`INSTR(',' |
| LIMIT offset,count | FETCH FIRST count ROWS ONLY / OFFSET ... ROWS FETCH | 兼容性视版本而定 |
还有一点经常被忽略:MySQL的字符串和数字比较会自动隐式转换,比如WHERE col = 123,如果col是VARCHAR,MySQL会把col转成数字比较。达梦在这类场景下有时会报“无效的数值”,有时会走索引失效。迁移SQL时最好把条件写得更明确,避免依赖数据库自动转换。
5.3 如何批量改写存储过程才不崩溃
迁移几十个存储过程时,不建议一个一个人肉改,也不建议全部用工具自动转。我的办法是分成两步:
先用正则做一批机械替换。比如把IFNULL(换成NVL(,把LIMIT n统一标出来,把DECLARE cur CURSOR FOR整理成规范格式。这一步能把50%的重复劳动解决掉。
然后打开达梦的管理工具,逐个创建每个存储过程,靠编译报错来逼出剩余问题。达梦有DBMS_UTILITY.FORMAT_ERROR_BACKTRACE,编译失败后会显示第几行错误,大多数情况下看到“语法错误”就知道是哪类问题。
更保险的方式是建立一个空表来记录每个对象的迁移状态:
| 对象名 | 类型 | 原MySQL行数 | 改写耗时 | 编译状态 | 备注 |
|---|---|---|---|---|---|
| sp_get_user | PROCEDURE | 35 | 20分钟 | 通过 | 改成PL/SQL结构 |
| trg_user_info_upd | TRIGGER | 12 | 15分钟 | 通过 | 时间戳逻辑重写 |
每次迁移完一个对象就打勾,最后统一做测试。这样心里有底,不会出现“迁移完了但不知道哪个过程编译失败”的状态。
6. 迁移失败的排查链路:从日志到数据核对
迁移过程中八成会碰到各种报错,遇到问题别慌,先看日志,再定位,最后核数据。这套链路能解决大多数迁移故障。
6.1 最容易踩的坑和日志定位法
常见迁移报错大概有这几类:
- 连接MySQL失败:驱动不存在、驱动类名不对、MySQL端不允许远程连接、SSL握手失败。
- 建表失败:字段类型不存在、默认值函数不兼容、字符集不支持。
- 数据导入失败:数值溢出、字符串太长、日期格式不对、NULL与空串混淆。
- 存储过程编译失败:语法差异、变量未声明、系统函数不存在。
排查时优先看达梦服务端日志,位置一般在达梦数据目录的/log下,文件名类似dmserver_xxxx.log。实时跟踪的方法:
tail -f $DM_HOME/log/dmserver_*.log如果是通过DTS工具迁移,工具自己的日志里会有更清晰的错误描述。我遇到过一个典型问题:源库字段是DATETIME,值是0000-00-00 00:00:00,达梦默认不允许这种非法日期,导入直接报错。解决方法有两个,要么在MySQL端把这类值改成NULL,要么在达梦端的表的日期类型上加上ALLOW_INVALID_DATES兼容项。后者不是万能的,很多版本还是会拒绝,所以最好在数据清洗阶段处理掉。
6.2 数据一致性校验:不能只数行数
行数一样不代表数据一样。我见过最坑的情况是CHAR字段尾部空格被自动去掉,导致内容比对不一致;还有浮点字段四舍五入后产生微小差异。所以迁移完成后做三轮校验。
第一轮校验行数:
-- 源库 SELECT COUNT(*) FROM db1.t1; -- 达梦 SELECT COUNT(*) FROM t1;第二轮校验关键字段的摘要,可以先生成MD5校验值。比如对一个大表取某些数值列的和、最大值、最小值,快速找出偏移:
-- 源库 SELECT COUNT(*), SUM(amount), MIN(create_time), MAX(create_time) FROM payment_record; -- 达梦 SELECT COUNT(*), SUM(amount), MIN(create_time), MAX(create_time) FROM payment_record;如果完全一致,基本可以放心。如果希望更精细,可以对每个字段做一次CHECKSUM聚合,比如用BIT_XOR或MD5拼接字符串。网上有现成的MySQL全库校验脚本,可以改造成达梦版,但要注意达梦的字符串拼接函数和聚合函数差异。
第三轮做抽样明细比对,选几条关键业务记录,整行导出后对比。这一步最好通过应用侧的测试人员一起参与,因为他们更清楚业务上哪些字段不允许误差。
7. 迁移后的应用适配与性能调优
数据库搬完不等于项目完工,应用连接和SQL性能往往才是真正的“最后一公里”。连接不上达梦、连接池狂报错、原来秒出的SQL现在卡死,这些我都遇到过。
7.1 JDBC驱动和连接池参数调整
达梦提供了JDBC驱动DmJdbcDriver18.jar,主流版本对应Java 8+。应用侧要把原来的MySQL驱动换成达梦驱动,并修改驱动类名和连接串:
// MySQL Class.forName("com.mysql.cj.jdbc.Driver"); String url = "jdbc:mysql://127.0.0.1:3306/dbname"; // 达梦 Class.forName("dm.jdbc.driver.DmDriver"); String url = "jdbc:dm://127.0.0.1:5236/dbname";如果应用用的是连接池(比如HikariCP、Druid),注意达梦的默认端口是5236,连接串格式是jdbc:dm://ip:port/schema。Druid里还要配置connection-properties,很多时候需要加上compatibleMode=mysql以及caseSensitive等参数。这里建议到达梦官方文档里查对应你版本的最佳参数组合,不要照搬社区里老旧的配置。
如果应用之前用了MyBatis/JPA,XML里某些MySQL专用SQL要逐条检查。特别是分页查询:MySQL用LIMIT,达梦支持LIMIT还是需要改成FETCH FIRST ? ROWS ONLY取决于版本和兼容模式,稳妥做法是改用物理分页插件,或统一写成WHERE ROWNUM <= ...的旧式写法。
7.2 常见慢SQL的改写思路
结构迁移后,原MySQL的索引可能在达梦上变得无效或者失效了。很可能出现同一个SQL在MySQL走索引很快,到达梦却全表扫。原因通常是:
- 字段类型变了,隐式转换导致索引失效。
- 字符集排序规则不同,导致无法利用索引范围扫描。
- 达梦优化器对某些运算符支持不如MySQL,比如
LIKE '%xx%'。 - 函数依赖索引,如
DATE(create_time) = ?,如果没建函数索引就慢。
遇到慢SQL,先通过EXPLAIN看执行计划:
EXPLAIN SELECT * FROM payment_record WHERE user_id = 123;对比达梦执行计划里是CSCN(全表扫描)还是CSEK(索引查找),然后判断是否要给SQL加上正确的索引。达梦的索引命名最好不要和MySQL的索引名冲突,迁移时要注意索引名唯一性。我在项目里遇到过两个表有同名索引,建第二个索引时报“对象已存在”,就是因为MySQL不同表索引名可以重复,而达梦的普通索引对象在Schema内一般是唯一的。解决方法是改索引名,或者借助工具在迁移时统一加上表名前缀。
性能验证方面,建议把线上核心SQL在达梦环境跑一遍压测,对比前后95分位耗时和吞吐量。如果差距明显,优先看不变量:执行计划、索引状态、统计信息。达梦的统计信息也像Oracle一样需要定期收集,刚导入完数据后统计信息可能不准确,记得执行DBMS_STATS.GATHER_TABLE_STATS或达梦的管理工具里“更新统计信息”功能,否则优化器会产生非常离谱的执行计划。
另外,应用侧的超时时间也要调整。原MySQL连接串可能有connectTimeout=3000等参数,连达梦时如果网络质量一般或者首次建连要做SSL握手,很容易超时。我习惯把connectTimeout放到5秒以上,socketTimeout根据业务调用时长调整,避免迁移后应用“假死”。
迁移这个活儿,从来不是把数据倒过去就完事,而是一个从结构到数据再到应用的系统工程。我个人的体会是:先把流程拆碎,每走一步都留痕、核对,比憋大招一次成功靠谱得多。最后再分享一个小习惯:每天迁移工作结束时,把当天的报错截图、日志片段和解决思路整理成简短的笔记,遇到重复问题直接翻笔记,效率会翻倍。你接手下一个迁移项目时,这笔“经验账”会比自己反复踩坑值钱得多。