MySQL迁移达梦数据库全流程实战:结构、数据、SQL方言到应用适配
2026/9/7 18:10:54 网站建设 项目流程

这两年国产化数据库替换的项目越来越多,MySQL到DM(达梦)的迁移几乎成了标配任务。老实说,我第一次接到迁移任务时也以为就是“导出表结构、灌入数据”两小时收工的事,真上手才发现从字段类型、自增列、存储过程到JDBC连接串,处处有坑。这篇文章是我做了多个迁移项目后沉淀下来的一套完整流程,覆盖迁移前的准备、工具选型、结构迁移、数据搬家、SQL方言改写、失败排查到最后的验证与适配,尽量把能踩的坑提前标出来。如果你正在处理“MySQL迁达梦”的活儿,这篇应该能让你少走几段弯路。

1. 迁移调研与准备:先别急着装工具

很多人拿到迁移任务第一件事就是打开DTS工具拖拽表,然后失败、报错、网上搜解决方案,折腾两天发现是流程顺序反了。迁移之前真正该做的是先盘清楚自己的MySQL到底长什么样,再决定达梦这边怎么初始化。

1.1 盘点源端MySQL的“家底”

这一步不涉及任何数据库迁移技术,但对后续影响巨大。我会拿个表格逐项登记,确保心里有数:

检查项具体要确认的内容为什么重要
MySQL版本5.7还是8.0,官方版还是云厂商RDS5.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=4

COMPATIBLE_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.Drivercom.mysql.cj.jdbc.Driver

2.2 为什么推荐“半自动”的组合策略

我会把工具分成几类:结构迁移工具、数据同步工具、命令行工具。中小型表用DTS全自动;大表和复杂对象用“手工SQL + 命令行工具”半自动处理。原因很简单,工具适合批量、重复、简单流程,但碰到真正棘手的大对象时,工具的处理策略往往不是最优解。

比如一张几亿行的日志表,DTS一条一条INSERT进去会非常慢,这时候用达梦的dmfldr高速加载工具或者dmexp/dmpimp反而更有优势。所以我的迁移流程通常这么安排:

  1. 先用DTS同步所有小表和中等表,完成后立即校验。
  2. 大表单独导出成文本文件,再通过dmfldr批量导入。
  3. 存储过程、函数、触发器、视图等手工迁移,不指望工具自动转。
  4. 最后通过SQL脚本补索引、约束、注释和触发器等附加对象。

这样做的逻辑很清楚:让工具处理它擅长的事,复杂场景人为介入,避免“一刀切”带来的返工。

3. 结构迁移的逐点对照:数据类型、约束与默认值

这部分是整个迁移中最容易被低估的环节。表面看都是CREATE TABLE,但MySQL和达梦的字段类型体系差异很大,直接照搬要么报错,要么生成性能很差的表结构。

3.1 MySQL到DM的数据类型映射表

下面是一份我整理过的常用映射参考,按经验不断修正过,可以当个速查表:

MySQL类型达梦类型说明
TINYINTTINYINT / SMALLINT如果TINYINT(1)表示布尔值,建议迁移为SMALLINTBIT,避免应用层取值类型混淆
SMALLINTSMALLINT无符号类型建议升一级到INT,因为达梦对无符号支持比较弱
MEDIUMINT / INT UNSIGNEDINT / BIGINT无符号INT必须升为BIGINT,否则溢出
BIGINTBIGINT注意达梦的BIGINT范围与MySQL一致
DECIMAL / NUMERICDECIMAL / NUMERIC精度、标度保持一致即可
FLOAT / DOUBLEFLOAT / DOUBLE注意浮点比较的误差行为,必要时改为DECIMAL
CHAR / VARCHARCHAR / VARCHAR注意字符长度单位,达梦默认按字符计算,和MySQL的字符语义一致,但多字节字符集下要确认
TEXT / TINYTEXT / MEDIUMTEXT / LONGTEXTTEXT / CLOB达梦TEXT等价于CLOB,长文本类型建议用CLOB
BLOB / BINARY / VARBINARYBLOB / VARBINARY二进制类型也要按长度和使用方式调整
DATETIMETIMESTAMP / DATETIME达梦有DATETIME,也可以映射TIMESTAMP
TIMESTAMPTIMESTAMP注意MySQLON UPDATE CURRENT_TIMESTAMP行为达梦不支持
DATEDATE好迁移
JSONCLOB / TEXT达梦有JSON类型(不同版本支持度不同),建议保守地映射为TEXT或CLOB,应用层再自己解析
ENUM / SETVARCHAR / 自定义约束建议转换为VARCHAR,并在应用层维护枚举值;否则通过CHECK约束管理有限值
GEOMETRYST_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_TIMESTAMPON 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,而文本里用了空字符串表示,dmfldrTRAILING 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是客户端命令,达梦不需要。
  • 达梦的变量声明在ASBEGIN之间,MySQL的DECLAREBEGIN内。
  • 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 <= 10PRINT 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_SUBDATEADD(d, n, date)参数顺序不同
FIND_IN_SET(str, list)无内置等价需要改写为`INSTR(','
LIMIT offset,countFETCH 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_userPROCEDURE3520分钟通过改成PL/SQL结构
trg_user_info_updTRIGGER1215分钟通过时间戳逻辑重写

每次迁移完一个对象就打勾,最后统一做测试。这样心里有底,不会出现“迁移完了但不知道哪个过程编译失败”的状态。

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根据业务调用时长调整,避免迁移后应用“假死”。

迁移这个活儿,从来不是把数据倒过去就完事,而是一个从结构到数据再到应用的系统工程。我个人的体会是:先把流程拆碎,每走一步都留痕、核对,比憋大招一次成功靠谱得多。最后再分享一个小习惯:每天迁移工作结束时,把当天的报错截图、日志片段和解决思路整理成简短的笔记,遇到重复问题直接翻笔记,效率会翻倍。你接手下一个迁移项目时,这笔“经验账”会比自己反复踩坑值钱得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询