MySQL到达梦数据库迁移:SQL兼容性实战与踩坑指南
发布时间:2026/10/13 4:27:03 锦皓数字建站

前两个月我们刚把一个跑了两年的 MySQL 业务系统迁到达梦数据库DM。刚接到需求时说实话我并没有太当回事数据导过去、连接串换掉、再挑几个明显不一样的语法改一改按经验应该一周内就能搞定。真正动手才发现这个判断错得离谱。整个迁移过程里真正消耗人力、需要逐条确认的不是数据搬运而是 SQL 语法兼容性——达梦的报错信息又经常说得含含糊糊有时候一条 SQL 翻过来覆过去排查一整天最后发现只是个小写表名的问题。这篇东西不打算写成官方文档的复述而是以这次真实迁移为线索把遇到的 SQL 语法差异、改写思路、迁移方案设计、以及上线前踩进去又爬出来的坑完整梳理一遍。适合正在评估或者已经启动 MySQL 到达梦迁移的团队也适合刚接触达梦、想提前了解兼容性边界的朋友。先说结论迁移本身不难难的是你永远不知道下一条 SQL 会在哪里出事。1. 先搞清楚一个前提达梦对 MySQL 的兼容和你以为的不一样1.1 兼容模式能兜底但兜不住全部很多人的第一反应是达梦不是号称兼容 MySQL 吗直接迁移不就行了——兼容是有的但要分层次看。达梦可以通过初始化参数设置不同的兼容模式切到 MySQL 兼容模式之后一部分基础语法确实能直接跑通比如简单的 SELECT、INSERT、UPDATE以及常规的 LIMIT 分页。但问题在于你根本不知道哪条 SQL 会在哪个位置突然踩到边界。举个例子MySQL 里很普通的INSERT ... ON DUPLICATE KEY UPDATE在达梦里并不会因为开了兼容模式就自动支持绝大多数场景还是要手工改成MERGE INTO。类似这种看文档好像支持、实际一跑就报错的语法才是迁移中最费时间的部分。我们第一次试迁移时结构迁移很顺利结果应用一启动日志里一片一片的 SQL 异常光看错误码根本猜不出是语法问题还是数据问题。所以我给这次迁移定的第一条原则是把兼容模式当作辅助手段不要把希望全押在它身上所有 SQL 都要过一遍真实执行而不是只看语法层面是否翻译成功。1.2 迁移前先做存量 SQL 盘点不盘点就直接开跑后面一定返工。我用三天时间把存量 SQL 全部捞了一遍来源包括后台服务的 MyBatis XML 文件、代码里拼接的动态 SQL、历史遗留的存储过程、备份的慢查询日志。最后统计下来一个不算大的业务系统需要人工确认的 SQL 接近一千条。这一步不是为了逐条去改而是为了估算工作量和找出高危区域。按我的经验最值得优先盘点的有三类分页查询、带聚合函数的统计 SQL、涉及自增主键和唯一键冲突的写入 SQL这三类基本上集中了 80% 的语法兼容问题。盘点后我按风险做了个简单分类SQL 类别典型风险处理建议分页查询排序被忽略、ROWNUM 叠加错误统一走分页组件方言复杂手写拆成双层嵌套聚合函数GROUP_CONCAT、IF 等函数不存在提前建函数映射表逐条替换写入类 SQLON DUPLICATE KEY UPDATE 不支持改 MERGE、提前确认业务语义自增主键相关建表语法、重置自增值方式不同迁移前锁定 IDENTITY 建表方案有了这张表后续排期就靠谱多了。每条 SQL 被标记为直接可用小改动需要重构三个等级重构优先级明确排到最前面。2. SQL 语法兼容问题清单跑一遍真实的达梦逐类解决下面这些语法差异是我这次迁移中实际遇到并手工改掉的每条都附了对应的改写思路大家可以直接对照自己项目里去查。2.1 分页与行数限制LIMIT 的可用但不完全可用分页是重灾区。MySQL 里最标准的写法是LIMIT #{offset}, #{size}达梦在简单场景下也认这个语法直接跑没问题。但把排序、子查询、去重组合起来之后事情就变了。比如这条 SQLMySQL 跑得好好的SELECT * FROM ( SELECT id, name, create_time FROM t_user ORDER BY create_time DESC ) t LIMIT 10, 20;到达梦里如果兼容模式没覆盖到位内层子查询的ORDER BY可能被忽略导致分页结果乱序或者直接报语法错误。处理方式是改成基于 ROWNUM 的双层嵌套SELECT * FROM ( SELECT t2.*, ROWNUM AS rn FROM ( SELECT id, name, create_time FROM t_user ORDER BY create_time DESC ) t2 WHERE ROWNUM 30 ) t WHERE rn 10;这个改写思路的本质是先在内层排好序并截断到上限再在外层把偏移量过滤掉语义和 MySQL 的LIMIT 10, 20完全对齐。这里有个实操细节内层的ROWNUM 30不能直接写成ROWNUM 10 AND ROWNUM 30因为 ROWNUM 在行号还没分配完成时做大于判断会出问题必须先捞出来再在外层过滤。另外提醒一句如果项目里用了 PageHelper不要手写这种嵌套分页直接把分页方言显式配置成 dm让框架自己生成兼容语句比自己改写省事得多。2.2 内置函数差异不是所有函数名都能照搬这是看起来最容易、实际上最坑的一类。因为函数名可能一样但参数个数、返回值类型、空值处理逻辑都不一样。MySQL达梦 DM改写要点IFNULL(a, b)NVL(a, b) / COALESCE(a, b)IFNULL 在某些兼容模式下可用但写成 NVL 最稳CONCAT(a, b, c)CONCAT(a, b) 或 a || bMySQL 支持多参数达梦的 CONCAT 参数个数有限用GROUP_CONCAT(expr SEPARATOR ,)LISTAGG(expr, ,)分隔符位置和 ORDER BY 写法差异很大DATE_FORMAT(d, %Y-%m-%d)TO_CHAR(d, YYYY-MM-DD)格式符完全不通用不能直接照搬STR_TO_DATE(s, %Y-%m-%d)TO_DATE(s, YYYY-MM-DD)参数顺序注意别抄错NOW() / CURDATE()SYSDATE / CURRENT_DATENOW() 也能用返回类型和精度有差异SUBSTRING_INDEX(s, d, n)INSTR SUBSTR 组合没有直接对等函数逻辑要展开写IF(cond, a, b)CASE WHEN cond THEN a ELSE b END写 CASE 最保险跨库通用最隐蔽的是CONCAT和||的空值语义。MySQL 的CONCAT(NULL, a)返回 NULL而 Oracle 系的||会把 NULL 当成空字符串拼接。业务里如果正好依赖这个行为改写后结果会静悄悄地变掉不对比数据根本发现不了。我的做法是先整理一份包含函数名、参数个数、空值行为、返回类型的映射表发给所有开发改写时按表执行避免各写各的带来新的不一致。2.3 自增主键与 DDL建表语句不是换个字段类型就完事MySQL 建表自带 AUTO_INCREMENT 和 COMMENT达梦这边的习惯是 IDENTITY 列建表语法差别挺大。-- MySQL CREATE TABLE t_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL COMMENT 订单号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT NOT NULL DEFAULT 0, amount DECIMAL(12,2) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;迁到达梦大体会改成CREATE TABLE t_order ( id BIGINT IDENTITY(1,1) PRIMARY KEY, order_no VARCHAR(32) NOT NULL, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, status SMALLINT NOT NULL DEFAULT 0, amount DECIMAL(12,2) ); COMMENT ON TABLE t_order IS 订单表; COMMENT ON COLUMN t_order.order_no IS 订单号;这中间有几个点很容易忽略。第一TINYINT到达梦后的映射类型可能变成 SMALLINT范围变大倒无所谓但索引和存储空间跟着变大批量表要注意。第二MySQL 的ALTER TABLE t AUTO_INCREMENT n在达梦没有直接等价写法涉及历史数据迁移时要用显式 ID 插入把自增起点同步到正确位置。如果目标表已经建了 IDENTITY 列迁移历史数据前先执行SET IDENTITY_INSERT 表名 ON插完再关掉不然显式 ID 会被无视。第三达梦对关键字比较敏感MySQL 里能当列名的comment、level、type这类词到达梦可能要加双引号否则建表直接报错。还有个容易忽略的是DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP的区别。MySQL 的表字段支持更新时自动刷新时间戳达梦默认没有这种贴合场景的等价行为改写时需要在前端或应用层把更新时间字段显式赋值不然数据更新后时间不变对账时一脸懵。2.4 写入类扩展语法ON DUPLICATE KEY UPDATE 和 REPLACE INTO 的等价改写这两条语法在 MySQL 里非常常用达梦没有原生对应需要改成 MERGE INTO。以最常见的存在就更新不存在就插入为例MERGE INTO t_user t USING ( SELECT ? AS user_id, ? AS name, ? AS age FROM dual ) s ON (t.user_id s.user_id) WHEN MATCHED THEN UPDATE SET t.name s.name, t.age s.age WHEN NOT MATCHED THEN INSERT (user_id, name, age) VALUES (s.user_id, s.name, s.age);这里一定要说清楚一个语义差异MySQL 的REPLACE INTO是先删后插如果目标表有外键关联或者自增主键是其他表的外键来源实际效果不只是更新还会把原行的自增值整个换掉。改成 MERGE 之前要和业务确认到底是要纯更新还是确实需要删除重建。我们项目里就有个老接口依赖 REPLACE 的自增重置行为改写后行为变了好在是测试阶段发现的不然上线就是事故。另外INSERT IGNORE这种存在则跳过的写法达梦里可以用WHERE NOT EXISTS前置判断也可以只保留 MERGE 的WHEN NOT MATCHED分支具体看并发要求和性能预期来选。2.5 字符集、长度单位和大写规则三个容易被忽略的隐性坑第一个坑是长度单位。MySQL 的VARCHAR(255)默认是 255 个字符而达梦的VARCHAR(255)在默认参数下可能按字节计算中文场景下 255 个汉字直接超长插入必报错。这个我在初始化数据库时就重点确认了达梦初始化时有参数控制 VARCHAR 长度是按字符算还是按字节算选按字符对应参数设置生效能在最大程度上保持和 MySQL 一致的语义。强烈建议在建任何业务表之前就确认好建完一堆表再改代价很大。第二个坑是标识符大小写。达梦在默认行为下未加引号的标识符会转成大写而 MySQL 在部分环境下对表名大小写敏感。迁移完经常出现明明表存在查询说找不到的灵异问题其实就是 DDL 里写了小写表名或者应用代码里的驼峰表名在两边被拼成了不同大小写。我们的最终方案是建表 DDL 统一用不带引号的大写对象名应用侧的 SQL 也全部规范化不要在两边交叉使用引号和小写。第三个坑是中文排序。MySQL 的 utf8_general_ci 和达梦默认排序规则对中文、拼音的处理完全不同如果业务里依赖按名称排序的结果上线后排序顺序会变。有明确排序要求的查询建议在 SQL 里显式指定排序规则而不是依赖全局默认值。3. 迁移方案工具搬数据人工改 SQL两条腿走路3.1 整体步骤和各阶段时间分配我把整个迁移拆成了八个阶段这里不写虚的直接列出来存量盘点收集所有 SQL、评估改动量大约 3 到 5 个工作日环境准备初始化达梦库确认字符集、长度单位、兼容模式半天结构迁移用迁移工具生成基线 DDL人工修正自增列、注释、索引1 到 2 天数据迁移分批次导数据核对行数和抽样数据工时看数据量SQL 改写按函数映射表改 MyBatis XML 和动态 SQL逐条执行验证和结构迁移并行应用适配JDBC 驱动、连接池、ORM 方言、分页方言配置功能与数据校验业务回归、行数对比、关键字段抽样比对上线切换与回滚演练保持 MySQL 实例只读可切回提前演练切换流程这里想特别强调第 2 步。很多人开工第一件事是直接装库跑 DTS等到建表完成才发现长度单位不对、字符集不对返工成本极高。环境参数这一类前置配置最好在迁移第一天就由专人确认并写成文档后续所有人共用同一套标准。3.2 数据迁移工具怎么选以及要不要全信它达梦自带的迁移工具能从一个已存在的 MySQL 实例里迁表结构和数据我们用它跑通了绝大部分表。但能跑通和跑对是两回事。工具在做异构类型映射时会按照自己的规则处理比如tinyint unsigned映射成什么、datetime(6)映射成什么、text映射成什么这些规则不见得符合业务预期。我建议先拿几张典型的表做小规模试迁移把生成的 DDL 和类型映射拉出来核对一轮再决定哪些表用工具、哪些表手工建。大表迁移时不要一把梭直接跑。工具一般支持按条件分批建议按主键区间拆成多个任务单次失败的代价可控。迁移过程中如果报了缓冲区不足之类的错误多半是 CLOB/BLOB 大字段拖累内存把分批粒度调小比调工具参数更有效。如果迁移过程中还要做数据清洗、字段拼接或者业务数据源不止一个用 DataX 这类开源同步工具做中间层也行。两个工具定位不同达梦自带的工具偏向表结构加数据整体搬DataX 偏向结构化同步、可自定义转换。项目里选一个主力方案就行不需要都堆上去。3.3 应用层的四个改造点驱动、连接池、ORM、分页插件应用层改造看似简单实际上每个点都能单独踩出坑来。JDBC 驱动要换成达梦的驱动类连接串写成jdbc:dm://主机IP:5236/库名。这步没什么难度但要注意驱动包版本和数据库实例版本尽量一致。我这次就碰上驱动旧、数据库新导致日期类型读写异常的问题升级驱动后才恢复。连接池如果是 Druid要把数据库类型标识正确配好让连接池用对的数据库方言做检测和初始化不然监控面板里的 SQL 统计会变成一堆识别错误。连接检测 SQL 用SELECT 1就行两边都认。ORM 层面MyBatis 相对省心只要 XML 里不写数据库专属语法换驱动就能跑但 Hibernate 需要配置对应的达梦方言否则自动生成的 SQL 里会出现不适配的序列和分页写法。所以如果项目里自定义 SQL 偏多建议统一站在 MyBatis 这一侧去控制减少框架自动生成 SQL 带来的不确定性。分页插件方面PageHelper 明确支持达梦方言把dialect显式写成dm就行。千万别让它自动识别自动识别在早期版本里可能出现偏差生成出半截 MySQL 语法半截达梦语法的 SQL报错还特别难查。3.4 校验不能只比 count要比关键字段数据迁移完很多人习惯SELECT COUNT(*)对比一下觉得数量一致就大功告成。count 一致只能说明行数对得上不能说明内容对得上。我这次做了三层校验第一层是全表行数对比。第二层是对关键业务表做抽样抽取主键范围后比对关键字段的值。第三层是对涉及金额、状态这类敏感字段的表用拼接字段做全量求值对比——两边把同一组字段拼成字符串再算哈希对不上就说明有字段级差异。为了可回滚切换期间 MySQL 实例不要立刻销毁保留至少一个完整备份有条件就开一份全量快照。业务侧暂时不要设计复杂的双写方案迁移初期以读验证为主发现异常直接回切比在两边同时写数据再对账要简单得多。4. 几个从上线前踩坑现场提回来的问题4.1 迁移工具报失败但看不出哪个对象失败工具跑完一堆表后报告里可能有几十条失败记录点进去只提示执行失败完全不说是哪条 SQL 出错。这时候不要反复重跑整个任务正确的排查姿势是把报告中失败的对象清单导出来只对失败对象单独建一个最小化迁移任务同时开启工具日志把每个 SQL 的执行日志捞出来再看。我遇到过一个表失败是因为源表里有个字段名正好是达梦的关键字工具生成 DDL 时没加引号建表就失败了。单独定位之后加上引号才恢复正常。这类问题如果不缩小任务范围会被埋在几十个对象的大任务里很难定位。4.2 上线后首页接口偶发报错错误信息指向不存在的表这个坑最诡异。应用日志里明明写着表不存在但我用管理工具查表又能查到。查到最后发现是标识符大小写问题应用代码里用的是小写表名达梦默认把无引号标识符统一转大写而 DDL 建表时工具又生成了带双引号的小写表名两边大小写对不上。解决方案很简单把建表 DDL 统一改成不带引号的大写对象名应用侧也规范表名。但这个排查过程教训很深表不存在不一定是真不存在先查大小写和引号再查权限最后才查物理表是否存在。4.3 报表变慢是因为分页改写后走了错误的执行计划有一张报表表MySQL 时代分页很快迁移完在达梦上明显变慢。抓出 SQL 一看是因为我们手工把 ORDER BY 和 ROWNUM 嵌套改写之后优化器对内层排序的选择出现了偏差计划先做全表扫描再做行号过滤。处理办法是给排序字段和相关过滤条件补上组合索引同时把内层 SQL 的执行计划单独 EXPLAIN 一遍确认排序走的是索引路径。达梦对复杂嵌套 SQL 的优化能力不像 MySQL 在某些场景下那么直给改写后一定要看执行计划不能只看 SQL 能不能跑通。4.4 批量插入慢到不可接受罪魁是逐条提交源库的批量插入代码在 MySQL 下很快换到达梦后慢了一个数量级。排查下来不是语法问题而是 JDBC 驱动对批量提交的支持行为不同应用代码里的批量插入变成了逐条执行事务提交次数爆炸。通过调整连接串参数和批量策略后恢复正常。这类问题不体现在语法报错上而体现在性能上迁移测试阶段要专门做一轮批量场景的压测别等到上线才测。如果项目里有每天定时灌数的作业这个坑几乎必踩提前压测能省下大量上线后的排查时间。5. 迁移验收清单与一点个人经验5.1 验收时逐项过的检查清单切换完成后我们基于整个迁移过程沉淀了一份验收清单。内容包括迁移报告里所有失败项归零数据行数与关键字段哈希对比全部通过核心业务回归用例 100% 通过慢查询日志里不再出现迁移后新增的异常 SQL应用日志无关键字冲突报错批量作业实测耗时回到可接受范围回滚切换演练通过。这份清单的关键是逐项留证据。每一项都附上执行日期、执行人、结果截图或者 log 片段这样后续复盘和审计都有据可依而不是凭一句当时测过没问题。5.2 回头看迁移中最值得提前做的事个人最大的体会是迁移 MySQL 到达梦本质上不是搬运数据而是翻译 SQL。前期花在盘点、建函数映射表、统一对象命名规范上的时间最终都会在测试和上线阶段以少踩坑的方式还回来。再分享一个具体建议从迁移第一天起就建一份共享的改写对照文档每当发现一个函数或语法差异立刻登记进去写清楚出现位置、兼容模式下的实际表现、最终改写方案。这个文档到后期就是整个团队的战场地图排查问题时先查它能省掉大量重复试错。如果你们正处在迁移选型阶段我建议先拿最复杂的十张表和最变态的十条 SQL 做一次小范围验证把改写成本估算出来再决定整体节奏。这比拿到需求就全量开工要稳妥得多也是这次迁移让我印象最深的一条经验。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。