MySQL迁移达梦DM8:SQL语法兼容与改造实战指南 最近带团队做了一轮存量系统从 MySQL 迁移到达梦数据库DM8的活儿说句实话真正难住人的不是数据搬移而是业务 SQL 的语法兼容。MySQL 写惯了的反引号、AUTO_INCREMENT、LIMIT 分页到了达梦这边连建表语句都跑不起来报错一个接一个光无效的表名语法错误就够你排查一上午。这篇文章我打算把这轮 MySQL 迁移达梦遇到的 SQL 语法问题、工具选型和整套改造方案完整梳理一遍给正在做数据库迁移或者准备适配达梦的 DBA、后端开发一份可以直接抄作业的落地清单。内容偏实战默认你已经用过 MySQL对达梦只有一个模糊认知看完至少能少踩一半坑。1. 迁移方案整体设计想清楚再动手1.1 达梦的兼容性定位决定改造量达梦 DM8 从架构和方言上看更接近 Oracle默认模式下绝大部分语法是按照 Oracle 风格来设计的。MySQL 里的反引号、ENGINE 子句、LIMIT 分页、ON DUPLICATE KEY UPDATE 这类写法默认模式基本都不认。达梦提供了兼容参数可以把实例调成更接近 MySQL 的兼容模式通过初始化参数里的相关配置来控制但即便开了兼容也不是所有 MySQL 语法都百分之百支持总有一些边界情况要手工处理。这就导致一个很现实的问题你的改造量主要由两个因素决定。一是达梦实例的兼容模式开没开二是应用里 SQL 写得多MySQL 原生。如果一开始就把兼容模式打开反引号、LIMIT、AUTO_INCREMENT 这些能少改一大半。如果 DBA 坚持用默认 Oracle 模式那所有 MySQL 特色语法都得推倒重写工作量是完全不同量级的。我的建议是做方案之前先和 DBA 确认清楚三件事达梦版本是多少、实例是否已开启 MySQL 兼容模式、字符集和大小写敏感策略是什么。这三个问题不搞清楚后面所有改造方案都是空中楼阁很可能你辛辛苦苦改完的 SQL换个实例又全部报错。1.2 我实际用下来靠谱的迁移流程我把整个迁移拆成六个步骤顺序不能乱。第一次做的时候我跳过结构直接导数据结果表结构不对数据导一半报错又删库重来白白浪费一整天。环境确认确认 DM 版本、兼容模式、字符集、大小写敏感规则最好把达梦的dm.ini里关键参数截图存档。结构迁移取出 MySQL 的建表语句做完整语法转换在达梦中先建库、建表、建索引。数据迁移用 DTS 工具或脚本把 MySQL 数据灌进达梦过程中记录大表、异常行。业务 SQL 改造对应用里的 SQL 做静态扫描和动态抓取逐条改写成达梦语法。功能回归跑核心业务链路重点对账、报表、分页列表。数据校验行数对比、关键字段抽样、聚合结果比对确保两边数据一致。这套流程看起来简单但每步都有不少细节。尤其是第 2 步结构迁移很多人以为拿个工具点一下就行实际上最能卡住进度的就是建表语句里那些 MySQL 专属语法下一章详细讲。2. 建表语句改造第一波语法风暴2.1 反引号、ENGINE、CHARSET 这些必错项MySQL 的建表语句长这样CREATE TABLE user ( id INT(11) NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL DEFAULT COMMENT 姓名, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;放到达梦默认模式下跑第一个错几乎都是反引号。达梦不认反引号它用双引号表示含特殊字符的标识符或者干脆不带引号。最省事的处理是把反引号全部去掉只要表名、列名不跟关键字冲突就不需要任何引号包裹。去掉反引号之后紧接着就是ENGINEInnoDB、DEFAULT CHARSETutf8mb4、COLLATEutf8mb4_bin这些 MySQL 引擎和字符集的专属子句达梦完全没有对应语法直接删除。我见过有人试图在达梦里找 InnoDB 等价物其实没意义达梦有自己的存储引擎体系MySQL 建表语句里的引擎声明对它来说就是无效语法。还有COMMENT用户表这种 MySQL 风格注释达梦默认不认。列定义后面的COMMENT 姓名同样不行需要把注释拆出来改成达梦的 COMMENT 语句COMMENT ON TABLE user IS 用户表; COMMENT ON COLUMN user.id IS 主键; COMMENT ON COLUMN user.name IS 姓名;这里有个隐蔽的坑如果建表脚本里列注释和表注释混在一起正则匹配容易误删建议先统一把 COMMENT 内容抽出来放到临时清单再生成 COMMENT ON 语句。我写过一个脚本专门干这事后面第四章再展开。2.2 自增列、整数宽度和数据类型映射表MySQL 里最常用的自增主键是id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY到达梦要改成id BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEYIDENTITY(1,1)就是达梦的自增列括号里前一个数是起始值后一个是步长。注意达梦一张表最多只能有一个 IDENTITY 列这个限制大多数业务都能接受。另外 MySQL 喜欢写INT(11)、BIGINT(20)括号里的 11 和 20 只是显示宽度不是存储长度达梦不接受这种写法直接写成INT、BIGINT即可。数据类型映射这是我迁移中反复对照的一张表基本可以覆盖 90% 的常规字段MySQL 类型达梦 DM8 类型备注TINYINTTINYINT兼容模式下可直接映射SMALLINTSMALLINTINT / INTEGERINT去掉后面的显示宽度BIGINTBIGINTDECIMAL(p,s)DECIMAL(p,s)精度标度基本一致VARCHAR(n)VARCHAR(n)确认实例字符集n 含义要实测TEXTTEXT注意达梦对大字段列的使用限制DATETIMETIMESTAMP最常用的映射TIMESTAMPTIMESTAMP默认值要调整DATEDATEBOOLEANTINYINT达梦没有原生布尔业务层按 0/1 处理BLOBBLOBTEXT 类型要单独说。MySQL 里喜欢对 TEXT 字段建前缀索引比如KEY idx_remark (remark(50))这种写法在达梦里是不支持的前缀索引不是 DM 的语法。遇到这种情况要么把索引改成普通索引但 TEXT 上建普通索引也可能受限要么干脆去掉索引在应用层换一种查询策略。我踩过的坑是原表在 TEXT 列上有一个前缀索引用来加速 LIKE 查询迁移到达梦后索引不能用后台列表页直接慢到超时最后改成在关联表里冗余一个关键字字段才解决。2.3 特殊列类型和索引的额外注意除了常规类型映射还有几个容易忽略的点。BIGINT UNSIGNED这种无符号类型达梦原生没有。如果原来表里的自增主键接近无符号上限迁过去用有符号 BIGINT 可能溢出。这个不能靠数据库层硬扛得在应用层改类型设计或者调整业务量预估。DATETIME DEFAULT CURRENT_TIMESTAMP在 MySQL 里很常见达梦可以接收TIMESTAMP DEFAULT SYSDATE但要注意实例时区。我遇到过一次源库是北京时间达梦默认是操作系统时间两边相差 8 小时迁移后所有时间字段都比原来早 8 小时排查了好久才发现是时区设置问题。索引名和约束名的冲突也值得留意。MySQL 里同一个库不同表的索引名可以重复比如每张表都来一个idx_status迁移到达梦后可能报同名对象已存在因为达梦对对象名的要求更严格。我在批量建表后就碰到这个问题最后统一给所有索引加表名前缀比如idx_user_status、idx_order_status再重新执行。还有一个点是外键。MySQL 里外键指向的列如果是前缀索引或者类型不一致迁到达梦照样会报错。建议迁移前先检查所有外键关联列的类型长度是否完全一致这一步在 MySQL 里经常被忽略到达梦会被严格校验。3. 业务 SQL 改造语法坑全记录3.1 分页写法LIMIT 与 ROWNUM 的纠缠MySQL 分页最顺手的就是LIMIT offset, size比如SELECT * FROM t ORDER BY id LIMIT 20, 10;达梦如果开启了 MySQL 兼容模式可能直接支持 LIMIT但我不想赌每个环境都开了兼容。更稳的写法是改成 ROWNUM 方案SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT * FROM t ORDER BY id ) t ) WHERE rn 20 AND rn 30;这里必须注意 ROWNUM 的机制它是在结果集生成时逐行分配的伪列不能直接写WHERE ROWNUM 20因为 ROWNUM 在条件判断时只对第一行返回 1条件ROWNUM 20永远不成立这样会查不到任何数据。必须先把数据查出来给 ROWNUM 起个别名再在外层做范围过滤。如果你的项目用了 MyBatis-Plus 或 PageHelper 这类分页插件优先考虑配置达梦方言。PageHelper 可以把helperDialect配成dm让框架自动生成对应的分页 SQL比手工改所有 mapper 里的 LIMIT 要省力得多。如果框架没有内置达梦方言可以先配 Oracle 方言跑通大多数场景但分页语句的生成逻辑可能有差异上线前一定要全量回归分页接口。3.2 高频函数替换对照业务 SQL 里的函数差异是第二大坑。我整理了一张高频函数替换表基本覆盖了常见需求MySQL 函数达梦替代写法说明DATE_FORMAT(d, %Y-%m-%d)TO_CHAR(d, YYYY-MM-DD)格式符从 %Y 变成 YYYYNOW()SYSDATE最直接的替换CURDATE()CURDATE() 或 SYSDATE建议统一用 SYSDATEIFNULL(a, b)NVL(a, b)语义一致IF(cond, a, b)CASE WHEN cond THEN a ELSE b END或 DECODEGROUP_CONCAT(col)LISTAGG(col, ,) WITHIN GROUP (ORDER BY col)用法差异最大STR_TO_DATE(s, fmt)TO_DATE(s, fmt)FIND_IN_SET(a, b)INSTR(b, a) 的变体写法需要小心处理分隔符DATEDIFF(a, b)达梦也存在 DATEDIFF但参数顺序建议实测先跑通验证再上线GROUP_CONCAT 是变化最大的一个。MySQL 里GROUP_CONCAT(name ORDER BY id SEPARATOR ,)用得很顺手达梦原生没有这个函数要用 LISTAGG 替代SELECT dept_id, LISTAGG(name, ,) WITHIN GROUP (ORDER BY id) FROM employee GROUP BY dept_id;注意 LISTAGG 的排序是写在WITHIN GROUP (ORDER BY ...)里不是放在参数里。如果原来的 GROUP_CONCAT 用了 DISTINCT达梦的 LISTAGG 在不同版本支持程度不一样我建议先用子查询去重再在外面做 LISTAGG这样最稳。FIND_IN_SET 也是 MySQL 的常用函数达梦里没有。做分账标签查询时我一般改成-- 原 MySQL WHERE FIND_IN_SET(vip, tags) 0 -- 达梦改写思路 WHERE CHARINDEX(, || vip || ,, , || tags || ,) 0这种写法的本质是把目标串前后补逗号再去查找子串能保证匹配完整标签而不是部分匹配。但要注意如果 tags 字段是索引列这样写会导致索引失效数据量大时要重新设计表结构否则查询会很慢。3.3 多表更新删除、UPSERT 等大型句式的改写这一部分是最容易被低估的。单表增删改查改起来容易一旦遇到多表关联更新、批量插入、UPSERT 这类大 SQL达梦和 MySQL 的写法差异会直接让应用崩掉。多行 INSERTMySQL 支持INSERT INTO t (a,b) VALUES (1,2),(3,4),(5,6);达梦默认模式不一定认这种多行 VALUES。如果报错改成INSERT INTO t (a,b) SELECT 1,2 FROM DUAL UNION ALL SELECT 3,4 FROM DUAL UNION ALL SELECT 5,6 FROM DUAL;DUAL 是达梦兼容 Oracle 的虚表数据量小的时候这样写完全没问题。数据量大或者并发高的话建议应用层改成批量单条插入避免一条 SQL 太长。UPDATE JOINMySQL 写关联更新很随意UPDATE a JOIN b ON a.id b.id SET a.name b.name WHERE b.status 1;达梦不支持这种写法要改成子查询形式UPDATE a SET a.name ( SELECT b.name FROM b WHERE b.id a.id ) WHERE EXISTS ( SELECT 1 FROM b WHERE b.id a.id AND b.status 1 );这个改写有个坑如果 b 表在 id 上不唯一子查询会返回多行达梦会直接报返回结果多于一行。所以关联列必须保证唯一如果业务上确实有可能重复要先对 b 表做去重用一个分组聚合后的结果来关联。DELETE JOINMySQL 支持DELETE FROM a USING a JOIN b ON a.idb.id WHERE b.flag0;达梦里同样不行改成DELETE FROM a WHERE EXISTS ( SELECT 1 FROM b WHERE b.id a.id AND b.flag 0 );注意这里只删 a 表的数据跟原语义一致。如果原 SQL 是要同时删两表的数据得拆成两条 DELETE放在同一个事务里保证原子性。UPSERTMySQL 的INSERT ... ON DUPLICATE KEY UPDATE在达梦没有直接对应要用 MERGE 改写MERGE INTO t USING ( SELECT 1 AS id, 张三 AS name FROM DUAL ) src ON (t.id src.id) WHEN MATCHED THEN UPDATE SET t.name src.name WHEN NOT MATCHED THEN INSERT (id, name) VALUES (src.id, src.name);达梦对 MERGE 的兼容性不错这类改写比想象中稳定。如果原来 MySQL 的 ON DUPLICATE KEY UPDATE 同时涉及多个唯一键改写时要特别注意 ON 条件只能有一个匹配依据多个唯一键的冲突处理逻辑要重新设计。REPLACE INTOMySQL 的REPLACE INTO t ...达梦不认识。它本质上是有则删旧插新会影响自增列的值改写建议直接转成 MERGE或者先 DELETE 再 INSERT 放在事务里。我第一次没注意这个后来被一个数据同步脚本反复打脸改完才消停。4. 迁移工具与批量提效4.1 DTS 数据迁移工具使用要点达梦自带的 DTSData Transfer Service工具是图形界面的一般在安装目录的tool下面可以从 MySQL 往 DM8 导数据。基本流程是先配置源端为 MySQL 类型填 IP、端口、账号密码再配置目标达梦连接选择要迁移的库表设置目标模式最后执行。实际使用中有几个点要特别注意。第一DTS 工具可能报找不到 MySQL 驱动需要手动把mysql-connector-java的 jar 包放到驱动目录或者通过图形界面指定 jar 包路径。第二表结构和数据能搬运但别指望它做完整的语法转换搬过去之后建表语句里的反引号、ENGINE 子句还是会原样报错结构还是得自己改。第三大表一定要分批导。我导过一张 3500 万行的订单表一次性全量跑了快两个小时中途网络抖动一次就断了后来改成按月份分批导每批 500 万行速度才正常断点续导也方便。DTS 还有一个细节大字段 TEXT、BLOB 在迁移时如果源端和目标的字符集不一致容易触发截断或者乱码。最好在迁移前先用一小批数据验证不要一上来就跑全量。4.2 批量替换脚本的思路如果项目里建表脚本和应用 SQL 有几百上千个文件手工改不现实。我实际用脚本做了几轮批量替换效率提升很明显。第一轮去反引号正则匹配反引号替换为空。第二轮清理 MySQL 专属尾部语法比如ENGINEInnoDB、DEFAULT CHARSETutf8mb4、COLLATEutf8mb4_bin全部删除同时把COMMENT xxx抽出来单独存成一个清单用于后面生成 COMMENT ON 语句。第三轮做类型替换把AUTO_INCREMENT换成IDENTITY(1,1)int(11)换成intdatetime换成timestamptinyint(1)换成tinyint。第四轮做函数替换把 IFNULL 换成 NVL、NOW 换成 SYSDATE、DATE_FORMAT 换成 TO_CHAR 这种逐条规则。下面是最基础的 sed 命令示例针对建表语句的常见替换sed -E s///g; s/ENGINE * *InnoDB//g; s/DEFAULT CHARSET * *[a-zA-Z0-9_]//g; s/AUTO_INCREMENT/IDENTITY(1,1)/g; s/int\([0-9]\)/int/g schema.sql schema_dm.sql这里强调一点条件替换的顺序很重要。要先把 COMMENT 抽出来再清理 ENGINE 和 CHARSET否则语句边界变了正则容易误伤到不该删的内容。我当时用 shell 加 perl 写了一条流水线跑完了整个项目的 DDL 转换前后也就十几分钟。但这套脚本只解决常规语法函数层的替换还是建议用更结构化的规则一个个验证过再上线。5. 高频问题排查实录5.1 连接层面的坑从 MySQL JDBC 切换到达梦最常见的报错是No suitable driver。这通常是因为驱动类和 URL 都没换。达梦的驱动类是dm.jdbc.driver.DmDriverURL 是jdbc:dm://IP:5236/DB_NAME注意端口默认是 5236不是 3306。Class.forName(dm.jdbc.driver.DmDriver); String url jdbc:dm://192.168.1.10:5236/DB_NAME;连接池参数也要清理。MySQL 连接串里常见的useSSL、serverTimezone、useUnicode这些参数达梦驱动不认配置上去要么报错要么被忽略最好全部去掉。另外达梦的账号权限体系跟 MySQL 不一样业务账号一般要单独授予CONNECT、RESOURCE角色不然建表、查询其他模式下的表都会报权限不足。我还遇到过应用账号无法在指定模式下建表的情况最后是 DBA 手动授了权限才解决。还有个容易忽略的点达梦的模式schema和用户强绑定默认用户下有一个同名 schema。MySQL 迁移过来的全限定名往往是dbname.tablename到达梦会解析成username.tablename如果目标模式名不对直接报无效的模式名。5.2 数据层面的坑数据迁移过程中最容易出问题的有三类。一是乱码。源端 MySQL 是 utf8mb4达梦实例初始化时如果选了 GBK 字符集迁过去中文就是一堆问号。这种问题在 DTS 迁移日志里看不明显但业务页面一打开就露馅。最靠谱的做法是在初始化 DM 实例时就统一用 UTF-8已经初始化成 GBK 的库改参数不一定生效最坏的情况是重新初始化再导一次。我们有一个系统就因为这个问题全部重来了一遍非常耽误时间。二是零日期。MySQL 允许0000-00-00 00:00:00这种零值日期达梦的 TIMESTAMP 不接受。DTS 迁移到一半在这个数据上报错是很典型的中断原因。迁移前先用 SQL 扫一遍所有日期字段把零日期统一改成 NULL 或者一个有效日期比如1970-01-01 00:00:00就能绕过去。三是超长字符。MySQL 的 VARCHAR 在某些排序规则下按字符计算长度达梦如果初始化成按字节计算原来够用的长度可能不够。最典型的是身份证号、手机号这种固定长度字段迁移时多出几个字节就报字符串超出长度。我的做法是迁移前统一把 VARCHAR 长度放大 20%~30%特别是那些线上已经出现过长数据的表。5.3 性能层面的坑数据迁过去了SQL 也能跑了不等于事情结束性能问题往往这个时候才冒出来。函数替换之后最容易导致索引失效。比如原来 MySQL 写DATE(create_time) 2024-01-01到达梦改写成TO_CHAR(create_time, YYYY-MM-DD) 2024-01-01如果 create_time 上有索引这种写法两边都很难走索引。更稳的写法是改成范围查询WHERE create_time TO_DATE(2024-01-01 00:00:00, YYYY-MM-DD HH24:MI:SS) AND create_time TO_DATE(2024-01-02 00:00:00, YYYY-MM-DD HH24:MI:SS)这样优化器才有机会走索引执行计划才可控。数据迁移完成后还要记得更新统计信息。MySQL 里 ANALYZE TABLE 用得不多因为优化器自动统计比较积极达梦这边更偏向 Oracle 风格刚导完数据如果不执行统计信息收集优化器可能选出很差的执行计划。我遇到过一次原来 MySQL 里几毫秒的查询到达梦跑出几十秒更新完统计信息后秒回。达梦里可以用DBMS_STATS.GATHER_SCHEMA_STATS(SCHEMA_NAME);最后是深翻页性能。ROWNUM 嵌套分页在大偏移量下性能很差比如偏移到 10 万行以后每页查询都会把前 10 万行全部扫一遍。建议结合业务改成基于游标的分页方式或者先查主键再关联原表。分页接口如果响应时间突然飙升优先查这一块。写到这里这轮 MySQL 迁移到达梦踩过的 SQL 语法坑和方案基本都覆盖了。我个人体会是迁移工具解决的是搬的问题真正决定项目进度的永远是 SQL 方言差异的改造量。如果时间允许建议先把应用的 SQL 全部收口到数据访问层不要散落在业务代码里否则改起来连影响范围都摸不清楚。最后分享一个小技巧迁移前先统一 MySQL 的 sql_mode把关掉严格模式期间产生的脏数据清理干净否则这些数据在达梦迁移过程中会频繁触发中断排查起来极其耗时。