这两年国产数据库迁移已经是个绕不开的话题,我经手过的项目里,从 MySQL 迁到达梦的频率越来越高。迁移最怕的不是数据量大,不是服务器配置低,而是那种看起来很正常、一跑到分页或者聚合查询就报错的 SQL 语法问题。很多团队把表结构和数据都搬过去了,最后却卡在 SQL 改写这个环节,一测业务流程就是满屏报错,项目周期硬生生被拉长一倍。这篇文章不打算讲那些“官方文档早就写明白”的基础概念,重点写我实际迁移过程中遇到的高频 SQL 语法坑,以及一套经过验证的迁移方案。内容偏实战,适合正在做国产化改造、或者准备从 MySQL 迁到达梦的 DBA、后端开发、数据工程师参考。1. 迁移前先把“兼容模式”和工具链想明白1.1 达梦的兼容模式不等于自动翻译器达梦数据库本身提供多种兼容模式,最常用的是 Oracle 兼容。很多人拿到达梦环境后以为开个 MySQL 兼容参数就能直接跑业务 SQL,实测下来并没有这么理想。达梦对 MySQL 语法的兼容是“部分兼容”,分页、自增列、反引号这些基本语法确实能覆盖一部分,但遇到存储过程、复杂聚合、隐式类型转换的时候,还是得按达梦自己的语法习惯改。所以我的建议是:迁达梦之前先开会确认一件事——目标库用哪种兼容模式。如果项目里大量代码是从 MySQL 生态带过来的,尽量让 DBA 评估一下兼容参数的实际覆盖范围。但无论如何,都要做好“SQL 改写是必然工作”的心理准备。兼容模式只是帮你少改一部分,不能当万能翻译器。这里还要提一个排查思路:迁移过程中如果某段 SQL 在测试库能跑、生产库报错,先别急着改代码,查一下两个库的兼容参数是否一致。这类问题我排查过好几次,最后基本都是参数不一致导致的。先把库级配置统一,再谈代码改写,否则你改完了这个库,换个环境又挂了。1.2 迁移工具选型:官方工具与半自动脚本达梦官方提供图形化的数据迁移工具,可以直接从 MySQL 数据源读取表结构、数据、视图和部分存储过程,生成目标库对象。实际用下来感受是:工具对表结构和基础数据的迁移完成度比较高,对存储过程、触发器、自定义函数的迁移成功率就一般了,好多对象是“迁过去了但语法不完整”,需要在目标库重新编译一遍。我的常规组合是:官方迁移工具负责表结构、序列、基础数据,自研脚本负责“工具没搬干净”的对象。比如我会先把所有存储过程从 MySQL 里导成文本文件,再写一个简单的正则处理脚本,帮忙做几件事:去掉反引号、去掉 ENGINE 和 CHARSET 子句、把DELIMITER重写为达梦能识别的格式、把AUTO_INCREMENT转成IDENTITY。这一步能省掉至少一半的手工工作量。工具选型的核心原则是:不要让工具替你决定业务逻辑。图形化工具擅长做“结构搬运”,但不擅长做“业务语义迁移”。复杂的 SQL 还是得靠人工逐条审视,工具只负责把大概率没问题的部分快速搬过去。2. 实战中最高频的 SQL 语法差异清单这一部分我会把迁移过程中最常遇到的语法差异列出来,每条都给出 MySQL 原写法和达梦改写方案。这些不是从文档里随手抄的,都是真实项目里踩过坑才总结出来的,优先级按出现频率排。2.1 分页查询:LIMIT/OFFSET 的两种改写MySQL 里写分页最简单的方式是:SELECT * FROM orders ORDER BY created_time DESC LIMIT 10 OFFSET 20;达梦在 MySQL 兼容模式下能识别这种写法,但生产环境很多用的是 Oracle 兼容模式,直接跑会报“关键字 LIMIT 附近有语法错误”。Oracle 兼容模式下,标准改写办法是三层嵌套配合 ROWNUM:SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT * FROM orders ORDER BY created_time DESC ) t WHERE ROWNUM <= 30 ) WHERE rn > 20;外层限制结束行号,内层限制起始行号,把LIMIT 10 OFFSET 20等价成“取第 21 到第 30 行”。这种三层写法确实啰嗦,但可读性和执行计划都相对可控。还有另一个办法:使用达梦新版本支持的OFFSET ... FETCH FIRST语法:SELECT * FROM orders ORDER BY created_time DESC OFFSET 20 ROWS FETCH FIRST 10 ROWS ONLY;这条语法更接近标准 SQL,理解成本低,但需要注意目标达梦版本是否支持。如果项目要求兼容多个版本的达梦环境,建议还是统一改成 ROWNUM 方案,省得在不同环境之间来回踩版本差异。2.2 自增主键:AUTO_INCREMENT 的迁移等价物MySQL 建表时最典型的写法是:CREATE TABLE member ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(64), PRIMARY KEY (id) );到达梦里,AUTO_INCREMENT不是标准语法,需要改成IDENTITY自增列:CREATE TABLE member ( id INT IDENTITY(1,1) NOT NULL, name VARCHAR(64), PRIMARY KEY (id) );IDENTITY(1,1)表示从 1 开始、每次加 1。这里有一个很容易被忽略的坑:MySQL 允许向自增列显式插入指定值,达梦的IDENTITY列在默认情况下不接受显式插入。如果迁移后的业务代码里有“先插入一条指定 ID 的数据”这种逻辑,会直接报错。解决办法是把表改成“非自增列 + 序列”的方式,或者临时关闭自增属性。实操中我更推荐用序列:CREATE SEQUENCE seq_member_id START WITH 1 INCREMENT BY 1; INSERT INTO member(id, name) VALUES (NEXT VALUE FOR seq_member_id, '张三');这种方案灵活性最高,既保留了自增能力,又不限制显式 ID 的插入。缺点是建表语句和插入语句都要相应调整,工作量比直接迁IDENTITY大一些,但稳定性好。2.3 函数替换对照表:从函数层面消除兼容隐患SQL 函数差异是仅次于分页的第二大坑。MySQL 和达梦的函数体系不完全一致,同名函数的参数含义甚至可能不同。我把高频遇到的替换关系整理成了表,方便直接对照:MySQL 写法达梦推荐写法说明IFNULL(a, b)NVL(a, b)含义相同,参数顺序也相同NOW()SYSDATE()/CURRENT_TIMESTAMP返回当前时间DATE_FORMAT(d, '%Y-%m-%d')TO_CHAR(d, 'YYYY-MM-DD')格式占位符完全不同,需要注意DATE_ADD(d, INTERVAL 1 DAY)DATEADD(DAY, 1, d)参数顺序相反DATEDIFF(d1, d2)DATEDIFF(DAY, d2, d1)达梦多了第一个“时间单位”参数GROUP_CONCAT(x)LISTAGG(x, ',')需要确认 LISTAGG 目标类型,避免截断CONCAT(a, b, c)CONCAT(a, b, c)或a || b || c达梦两种写法都支持,注意 `SUBSTRING_INDEX(s, '.', 2)改造为SUBSTR+INSTR组合没有直接等价函数FIND_IN_SET(s, 'a,b,c')改造为INSTR或关系表没有直接等价函数DATE_FORMAT到TO_CHAR的坑最隐蔽。MySQL 里%Y代表四位年份、%m代表两位月份;达梦里YYYY和MM才是对应的格式占位符。很多人迁移时只改了函数名,没改格式串,跑出来的日期直接变成“2024-24-24”这种诡异结果。GROUP_CONCAT替换成LISTAGG时还有一个类型问题。MySQL 的GROUP_CONCAT默认返回字符串类型,达梦的LISTAGG在某些版本下返回CLOB,程序端拿到CLOB后无法直接当字符串用。改写时建议显式加上长度限定:SELECT LISTAGG(name, ',' WITHIN GROUP (ORDER BY id)) AS name_str FROM member;如果发现返回类型不对,可以在外层套一个CAST(... AS VARCHAR(...)),提前把类型固定住,避免程序端报错。2.4 反引号、大小写与保留字MySQL 给表名、列名加反引号是习惯了,达梦不认反引号。迁移时最简单的处理方式是把反引号全部去掉,达梦在 Oracle 兼容模式下默认不区分大小写,小写表名会被自动转成大写标识符。听起来很省事,但埋了一个坑:如果你的 MySQL 表名用了驼峰命名或混合大小写,迁移到达梦后,MySQL 侧对小写敏感,达梦侧不敏感,两边查出来的对象名很可能对不上。我的处理原则是:建表阶段统一用小写命名并在迁移脚本里去反引号,然后让达梦自动转大写。这样程序里写小写表名也能正常匹配,不会出现“表不存在”的问题。如果因为历史原因保留了混合大小写,建议在达梦里用双引号包裹对象名,强制区分大小写,但这样后续所有 SQL 都要严格匹配大小写,维护成本高,不推荐。保留字问题也很常见。MySQL 里有些列名是合法标识符,到了达梦就成了保留字,比如comment、level、number、row。迁移时报“无效的标识符”多半就是这个原因。应对方案有两个:给列加双引号,或者直接改列名。我倾向于改列名,因为程序里对这些列的使用遍布各处,一旦加了双引号,后续每条 SQL 都要记得写英文双引号,很容易漏。2.5 连表更新/插入冲突:REPLACE 与 JOIN UPDATE 的重写MySQL 的INSERT ... ON DUPLICATE KEY UPDATE和REPLACE INTO在达梦中没有直接等价语法。需要改写成MERGE INTO:-- MySQL 写法 INSERT INTO product_stock(product_id, stock) VALUES(1001, 10) ON DUPLICATE KEY UPDATE stock = stock + 10; -- 达梦改写 MERGE INTO product_stock t USING (SELECT 1001 AS product_id, 10 AS add_stock FROM DUAL) s ON (t.product_id = s.product_id) WHEN MATCHED THEN UPDATE SET t.stock = t.stock + s.add_stock WHEN NOT MATCHED THEN INSERT(product_id, stock) VALUES(s.product_id, s.add_stock);MERGE INTO的语法结构比 MySQL 的写法复杂很多,但语义更严谨,尤其是“存在则更新,不存在则插入”这类场景,可以避免反复拼接多条 SQL。改写时要注意USING子句里的数据来源,如果是多行数据,可以用子查询直接从一个临时结果集关联。MySQL 的UPDATE ... JOIN在达梦里也要用MERGE INTO改写。比如“根据订单表统计结果更新用户表字段”这类逻辑:-- MySQL 写法 UPDATE user u JOIN (SELECT user_id, COUNT(*) cnt FROM orders GROUP BY user_id) o ON u.id = o.user_id SET u.order_cnt = o.cnt; -- 达梦改写 MERGE INTO user u USING ( SELECT user_id, COUNT(*) cnt FROM orders GROUP BY user_id ) o ON (u.id = o.user_id) WHEN MATCHED THEN UPDATE SET u.order_cnt = o.cnt;这类改写核心思路是:把 MySQL 的多表 UPDATE 拆成“目标表 + 源数据集”,通过关联条件绑定,再用WHEN MATCHED决定更新行为。2.6 存储过程、触发器与异常处理存储过程和触发器是迁移工作里最耗时的一环。MySQL 的存储过程支持DELIMITER作为脚本分隔符,达梦不认这个东西,而且达梦存储过程整体上是 Oracle 风格的块结构。老项目里如果存储过程特别多,建议先做一轮语法扫描,把所有DELIMITER、反引号、IFNULL、NOW()这类共性内容批量替换掉,再进行针对性的逻辑调整。变量声明方面,MySQL 写DECLARE v_count INT DEFAULT 0;,达梦的DECLARE位置和写法基本类似,但在 PL/SQL 块里常写成:CREATE OR REPLACE PROCEDURE test_proc AS v_count INT; BEGIN SELECT COUNT(*) INTO v_count FROM member; IF v_count > 0 THEN DBMS_OUTPUT.PUT_LINE('有数据'); END IF; END;异常处理差异也常踩坑。MySQL 通常用SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'xxx';主动抛错,达梦里可以改成RAISE_APPLICATION_ERROR(-20001, 'xxx');。捕获异常时,MySQL 写EXIT HANDLER FOR SQLEXCEPTION,达梦用EXCEPTION WHEN OTHERS THEN ...结构。如果存储过程里批量使用了这些写法,建议整理成统一模板,再从脚本层面做规则替换。触发器要格外注意新旧值引用。MySQL 用NEW.col和OLD.col,达梦同样支持这样的写法,但在某些场景下需要先声明对应变量再使用,否则直接引用会报“变量未声明”。我习惯的做法是在触发器的BEGIN前先声明:CREATE OR REPLACE TRIGGER trg_member_bi BEFORE INSERT ON member FOR EACH ROW BEGIN IF :NEW.id IS NULL THEN SELECT NEXT VALUE FOR seq_member_id INTO :NEW.id; END IF; END;注意达梦里NEW前要带冒号,写成:NEW,这和 MySQL 的NEW.不带冒号是有区别的。这个冒号很不起眼,但漏掉就会编译失败。3. 迁移实操全流程:结构、数据、业务三层迁移方案不能只看语法差异,还要有一整套从结构到数据再到业务验证的流程。我习惯把迁移拆成三个层面,逐层推进,每一层都有明确的产出物和验证标准。3.1 第一步:表结构和类型映射表结构迁移是基础,但类型映射比很多人想象得更复杂。MySQL 的类型和达梦类型不是一一对应,需要手动或工具自动做映射。高频映射规则我总结如下:MySQL 类型达梦推荐类型注意事项INT/INT(11)INT长度显示宽度直接去掉BIGINTBIGINT通常可以保持TINYINTSMALLINT或TINYINT达梦确实有 TINYINT,但部分场景建议扩宽VARCHAR(n)VARCHAR(n)确认长度是否超过达梦限制TEXTTEXT/CLOB大字段注意应用类型转换DATETIMETIMESTAMP存储精度范围要回测TIMESTAMPTIMESTAMP注意时区和默认值BOOLEAN/TINYINT(1)BIT或SMALLINTMySQL 的 0/1 语义需确认DECIMAL(m,n)DECIMAL(m,n)精度保留,避免溢出BLOBBLOB迁移后需测试读写建表脚本迁移时,还要把ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='表说明'这部分清理掉。达梦不支持这种 MySQL 特有配置,但不影响表注释,COMMENT可以保留。写一个简单的脚本扫描建表 DDL,删除引擎、字符集、自增列这些共性配置,能显著提高导入效率。3.2 第二步:分批导数据,尽量不要一把梭数据迁移最忌讳的就是用一条大 INSERT 把百万行数据一次性导入目标库。达梦对大数据量事务的处理能力和 MySQL 不一样,一把梭可能导致回滚段膨胀、日志暴涨、导入到一半失败。我从实测中总结的分批策略是:按主键或唯一键的范围切片,每批 5 万到 10 万行,一批一个事务提交。如果表没有主键,可以用ROWNUM或先抽取到临时表再加自增列。批量导入时优先使用达梦的批量装载工具,效率远高于逐条 INSERT;如果条件不允许,至少用 JDBC/驱动层的批量参数,把rewriteBatchedStatements之类的能力用起来。导入完成后必须做数据校验。最基础的是行数对比:SELECT COUNT(*) FROM source_table; SELECT COUNT(*) FROM target_table;行数一致不代表数据一致,建议对关键字段再做一次校验和对比。简单做法是分组统计核对总金额、最大值、最小值,复杂做法是用 MD5 拼接所有字段后做哈希比对。第一批迁移建议直接上哈希比对,宁可在校验上多花时间,也别把数据问题留到业务联调阶段暴露。3.3 第三步:SQL 回归清单和测试用例数据搬完以后,业务能不能跑通才是真正的考验。我建议在迁移前就整理一份 SQL 回归清单,把系统里所有高频 SQL 和关键存储过程列进去,逐条在达梦上执行并记录执行结果和耗时。回归清单至少应该包含这几类:分页查询 SQL(每个业务模块的主列表页)聚合统计 SQL(报表、首页仪表盘)关联查询 SQL(多表 JOIN、连表更新)存在唯一键冲突的写入 SQL所有存储过程、触发器、定时任务有了回归清单,迁移验证就变成一项可量化的操作。执行一遍清单,把报错的 SQL 集中改成达梦语法,改完再执行一遍,直到全部通过。这条清单后续也能当作上线前的验收文档,一举两得。4. 常见问题排查与案例复盘这一部分记录我实际排查过的典型案例。每个案例都包含现象、排查思路和最终解决方案,希望能帮你少走弯路。4.1 典型报错速查表报错信息可能原因处理方向“关键字 LIMIT 附近有语法错误”用了 Oracle 兼容模式改为 ROWNUM 嵌套或 OFFSET FETCH 语法“不是 GROUP BY 表达式”分组字段外的列没加聚合函数补齐聚合或调整分组字段“无效的标识符”列名是保留字,或反引号未清理去掉反引号,必要时改列名“自增列的显式插入被拒绝”对 IDENTITY 列插入显式值改成序列 + NEXT VALUE FOR 方案“字符串截断”VARCHAR 长度不够或 CLOB 转换问题调整字段长度,显式 CAST“LISTAGG 返回 CLOB 导致类型不兼容”版本聚合函数类型差异外层加 CAST 成 VARCHAR“过程编译失败”引用了 MySQL 特有语法检查 DELIMITER、异常处理、NEW 冒号4.2 案例一:分页查询在 Oracle 兼容模式下的连锁反应某管理后台的分页列表迁移到达梦后,前三页都能正常打开,第四页以后直接 500。排查后发现列表 SQL 用的是LIMIT 20 OFFSET 40,前三页的偏移量小,达梦在某种处理下勉强兼容,但一旦 OFFSET 变大就触发了语法解析问题。处理方式是把这个模块的所有分页 SQL 统一改成三层 ROWNUM 结构,并在数据库层面做了一次全量扫描,把所有含LIMIT的代码找出来集中改写。这里想提醒一下:兼容模式对 LIMIT 的支持在不同版本、不同参数下表现不一致,不要把“测试环境能跑”当作结论,最好直接用生产环境的兼容参数来验证。4.3 案例二:GROUP_CONCAT 聚合结果被截断某报表模块用GROUP_CONCAT把用户标签拼接成一个逗号分隔的字符串,在 MySQL 里一直正常。迁到达梦后,改成了LISTAGG,大部分用户数据正常,个别标签特别多的用户,报表程序端突然报字符串截断错误。排查后发现达梦的LISTAGG输出类型偏大,程序端拿到的是 CLOB 而不是 VARCHAR,在持久化到中间表时被截断了。最终处理是在LISTAGG外层套CAST(... AS VARCHAR(4000)),同时跟产品确认了字段长度上限。这个案例给我最大的教训是:函数迁移不能只看“能不能跑出结果”,还要看返回类型和长度边界,特别是聚合函数。4.4 案例三:条件字段隐式转换导致索引失效迁移完成后,某订单查询接口从 MySQL 的 300 毫秒变成了达梦的 8 秒。执行计划一查,发现查询条件里的订单号是字符串,而表里的字段类型是数字型,达梦对隐式类型转换的处理方式和 MySQL 有差别,导致索引失效走了全表扫描。解决方法是调整 SQL,让条件类型和字段类型保持一致,显式写成:SELECT * FROM orders WHERE order_id = CAST('100123456' AS BIGINT);这里想多说一句,MySQL 对隐式转换比较宽容,达梦对类型匹配的一致性要求更高。迁移回归清单里建议把“可能存在隐式类型转换的 SQL”单列一组,用EXPLAIN查看执行计划,把全表扫描的 SQL 挑出来重点优化。5. 写在最后的几个经验材料备齐、工具选好以后,迁移本身并不难,真正消耗时间的是问题复现和边界情况处理。我个人在实际操作中的体会是:语法差异清单要动态维护,每次遇到一个新的报错,就记录到清单里,并在文档里标清楚触发条件和解法。这样无论项目里来多少新人,都能快速上手处理同类问题,而不是反复交学费。另一个小技巧是建立“最小改写样例”。对于拿不准的语法,先在测试库建一张临时表,用最小化 SQL 验证达梦的行为,确认无误后再应用到完整 SQL 上。比如不确定LISTAGG返回类型是否会被应用端影响,就先跑一条最简单的聚合语句看看结果类型,再决定是否要加CAST。这种验证方式快且安全,比直接改大 SQL 后交给业务测试高效得多。迁移本身是一次非常好的技术复盘机会。把 MySQL 里的老 SQL 全部过一遍语法,你会发现自己项目里的 SQL 写得有多随意。达梦正好强制你把这些随意的地方收敛起来,从长远看对代码质量和数据库性能都有正向作用。如果后续还有别的系统要迁,前期整理出来的语法改写模板和回归清单可以直接复用,越到后面迁移速度越快。
这两年国产数据库迁移已经是个绕不开的话题,我经手过的项目里,从 MySQL 迁到达梦的频率越来越高。迁移最怕的不是数据量大,不是服务器配置低,而是那种看起来很正常、一跑到分页或者聚合查询就报错的 SQL 语法问题。很多团队把表结构和数据都搬过去了,最后却卡在 SQL 改写这个环节,一测业务流程就是满屏报错,项目周期硬生生被拉长一倍。这篇文章不打算讲那些“官方文档早就写明白”的基础概念,重点写我实际迁移过程中遇到的高频 SQL 语法坑,以及一套经过验证的迁移方案。内容偏实战,适合正在做国产化改造、或者准备从 MySQL 迁到达梦的 DBA、后端开发、数据工程师参考。1. 迁移前先把“兼容模式”和工具链想明白1.1 达梦的兼容模式不等于自动翻译器达梦数据库本身提供多种兼容模式,最常用的是 Oracle 兼容。很多人拿到达梦环境后以为开个 MySQL 兼容参数就能直接跑业务 SQL,实测下来并没有这么理想。达梦对 MySQL 语法的兼容是“部分兼容”,分页、自增列、反引号这些基本语法确实能覆盖一部分,但遇到存储过程、复杂聚合、隐式类型转换的时候,还是得按达梦自己的语法习惯改。所以我的建议是:迁达梦之前先开会确认一件事——目标库用哪种兼容模式。如果项目里大量代码是从 MySQL 生态带过来的,尽量让 DBA 评估一下兼容参数的实际覆盖范围。但无论如何,都要做好“SQL 改写是必然工作”的心理准备。兼容模式只是帮你少改一部分,不能当万能翻译器。这里还要提一个排查思路:迁移过程中如果某段 SQL 在测试库能跑、生产库报错,先别急着改代码,查一下两个库的兼容参数是否一致。这类问题我排查过好几次,最后基本都是参数不一致导致的。先把库级配置统一,再谈代码改写,否则你改完了这个库,换个环境又挂了。1.2 迁移工具选型:官方工具与半自动脚本达梦官方提供图形化的数据迁移工具,可以直接从 MySQL 数据源读取表结构、数据、视图和部分存储过程,生成目标库对象。实际用下来感受是:工具对表结构和基础数据的迁移完成度比较高,对存储过程、触发器、自定义函数的迁移成功率就一般了,好多对象是“迁过去了但语法不完整”,需要在目标库重新编译一遍。我的常规组合是:官方迁移工具负责表结构、序列、基础数据,自研脚本负责“工具没搬干净”的对象。比如我会先把所有存储过程从 MySQL 里导成文本文件,再写一个简单的正则处理脚本,帮忙做几件事:去掉反引号、去掉 ENGINE 和 CHARSET 子句、把DELIMITER重写为达梦能识别的格式、把AUTO_INCREMENT转成IDENTITY。这一步能省掉至少一半的手工工作量。工具选型的核心原则是:不要让工具替你决定业务逻辑。图形化工具擅长做“结构搬运”,但不擅长做“业务语义迁移”。复杂的 SQL 还是得靠人工逐条审视,工具只负责把大概率没问题的部分快速搬过去。2. 实战中最高频的 SQL 语法差异清单这一部分我会把迁移过程中最常遇到的语法差异列出来,每条都给出 MySQL 原写法和达梦改写方案。这些不是从文档里随手抄的,都是真实项目里踩过坑才总结出来的,优先级按出现频率排。2.1 分页查询:LIMIT/OFFSET 的两种改写MySQL 里写分页最简单的方式是:SELECT * FROM orders ORDER BY created_time DESC LIMIT 10 OFFSET 20;达梦在 MySQL 兼容模式下能识别这种写法,但生产环境很多用的是 Oracle 兼容模式,直接跑会报“关键字 LIMIT 附近有语法错误”。Oracle 兼容模式下,标准改写办法是三层嵌套配合 ROWNUM:SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT * FROM orders ORDER BY created_time DESC ) t WHERE ROWNUM <= 30 ) WHERE rn > 20;外层限制结束行号,内层限制起始行号,把LIMIT 10 OFFSET 20等价成“取第 21 到第 30 行”。这种三层写法确实啰嗦,但可读性和执行计划都相对可控。还有另一个办法:使用达梦新版本支持的OFFSET ... FETCH FIRST语法:SELECT * FROM orders ORDER BY created_time DESC OFFSET 20 ROWS FETCH FIRST 10 ROWS ONLY;这条语法更接近标准 SQL,理解成本低,但需要注意目标达梦版本是否支持。如果项目要求兼容多个版本的达梦环境,建议还是统一改成 ROWNUM 方案,省得在不同环境之间来回踩版本差异。2.2 自增主键:AUTO_INCREMENT 的迁移等价物MySQL 建表时最典型的写法是:CREATE TABLE member ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(64), PRIMARY KEY (id) );到达梦里,AUTO_INCREMENT不是标准语法,需要改成IDENTITY自增列:CREATE TABLE member ( id INT IDENTITY(1,1) NOT NULL, name VARCHAR(64), PRIMARY KEY (id) );IDENTITY(1,1)表示从 1 开始、每次加 1。这里有一个很容易被忽略的坑:MySQL 允许向自增列显式插入指定值,达梦的IDENTITY列在默认情况下不接受显式插入。如果迁移后的业务代码里有“先插入一条指定 ID 的数据”这种逻辑,会直接报错。解决办法是把表改成“非自增列 + 序列”的方式,或者临时关闭自增属性。实操中我更推荐用序列:CREATE SEQUENCE seq_member_id START WITH 1 INCREMENT BY 1; INSERT INTO member(id, name) VALUES (NEXT VALUE FOR seq_member_id, '张三');这种方案灵活性最高,既保留了自增能力,又不限制显式 ID 的插入。缺点是建表语句和插入语句都要相应调整,工作量比直接迁IDENTITY大一些,但稳定性好。2.3 函数替换对照表:从函数层面消除兼容隐患SQL 函数差异是仅次于分页的第二大坑。MySQL 和达梦的函数体系不完全一致,同名函数的参数含义甚至可能不同。我把高频遇到的替换关系整理成了表,方便直接对照:MySQL 写法达梦推荐写法说明IFNULL(a, b)NVL(a, b)含义相同,参数顺序也相同NOW()SYSDATE()/CURRENT_TIMESTAMP返回当前时间DATE_FORMAT(d, '%Y-%m-%d')TO_CHAR(d, 'YYYY-MM-DD')格式占位符完全不同,需要注意DATE_ADD(d, INTERVAL 1 DAY)DATEADD(DAY, 1, d)参数顺序相反DATEDIFF(d1, d2)DATEDIFF(DAY, d2, d1)达梦多了第一个“时间单位”参数GROUP_CONCAT(x)LISTAGG(x, ',')需要确认 LISTAGG 目标类型,避免截断CONCAT(a, b, c)CONCAT(a, b, c)或a || b || c达梦两种写法都支持,注意 `SUBSTRING_INDEX(s, '.', 2)改造为SUBSTR+INSTR组合没有直接等价函数FIND_IN_SET(s, 'a,b,c')改造为INSTR或关系表没有直接等价函数DATE_FORMAT到TO_CHAR的坑最隐蔽。MySQL 里%Y代表四位年份、%m代表两位月份;达梦里YYYY和MM才是对应的格式占位符。很多人迁移时只改了函数名,没改格式串,跑出来的日期直接变成“2024-24-24”这种诡异结果。GROUP_CONCAT替换成LISTAGG时还有一个类型问题。MySQL 的GROUP_CONCAT默认返回字符串类型,达梦的LISTAGG在某些版本下返回CLOB,程序端拿到CLOB后无法直接当字符串用。改写时建议显式加上长度限定:SELECT LISTAGG(name, ',' WITHIN GROUP (ORDER BY id)) AS name_str FROM member;如果发现返回类型不对,可以在外层套一个CAST(... AS VARCHAR(...)),提前把类型固定住,避免程序端报错。2.4 反引号、大小写与保留字MySQL 给表名、列名加反引号是习惯了,达梦不认反引号。迁移时最简单的处理方式是把反引号全部去掉,达梦在 Oracle 兼容模式下默认不区分大小写,小写表名会被自动转成大写标识符。听起来很省事,但埋了一个坑:如果你的 MySQL 表名用了驼峰命名或混合大小写,迁移到达梦后,MySQL 侧对小写敏感,达梦侧不敏感,两边查出来的对象名很可能对不上。我的处理原则是:建表阶段统一用小写命名并在迁移脚本里去反引号,然后让达梦自动转大写。这样程序里写小写表名也能正常匹配,不会出现“表不存在”的问题。如果因为历史原因保留了混合大小写,建议在达梦里用双引号包裹对象名,强制区分大小写,但这样后续所有 SQL 都要严格匹配大小写,维护成本高,不推荐。保留字问题也很常见。MySQL 里有些列名是合法标识符,到了达梦就成了保留字,比如comment、level、number、row。迁移时报“无效的标识符”多半就是这个原因。应对方案有两个:给列加双引号,或者直接改列名。我倾向于改列名,因为程序里对这些列的使用遍布各处,一旦加了双引号,后续每条 SQL 都要记得写英文双引号,很容易漏。2.5 连表更新/插入冲突:REPLACE 与 JOIN UPDATE 的重写MySQL 的INSERT ... ON DUPLICATE KEY UPDATE和REPLACE INTO在达梦中没有直接等价语法。需要改写成MERGE INTO:-- MySQL 写法 INSERT INTO product_stock(product_id, stock) VALUES(1001, 10) ON DUPLICATE KEY UPDATE stock = stock + 10; -- 达梦改写 MERGE INTO product_stock t USING (SELECT 1001 AS product_id, 10 AS add_stock FROM DUAL) s ON (t.product_id = s.product_id) WHEN MATCHED THEN UPDATE SET t.stock = t.stock + s.add_stock WHEN NOT MATCHED THEN INSERT(product_id, stock) VALUES(s.product_id, s.add_stock);MERGE INTO的语法结构比 MySQL 的写法复杂很多,但语义更严谨,尤其是“存在则更新,不存在则插入”这类场景,可以避免反复拼接多条 SQL。改写时要注意USING子句里的数据来源,如果是多行数据,可以用子查询直接从一个临时结果集关联。MySQL 的UPDATE ... JOIN在达梦里也要用MERGE INTO改写。比如“根据订单表统计结果更新用户表字段”这类逻辑:-- MySQL 写法 UPDATE user u JOIN (SELECT user_id, COUNT(*) cnt FROM orders GROUP BY user_id) o ON u.id = o.user_id SET u.order_cnt = o.cnt; -- 达梦改写 MERGE INTO user u USING ( SELECT user_id, COUNT(*) cnt FROM orders GROUP BY user_id ) o ON (u.id = o.user_id) WHEN MATCHED THEN UPDATE SET u.order_cnt = o.cnt;这类改写核心思路是:把 MySQL 的多表 UPDATE 拆成“目标表 + 源数据集”,通过关联条件绑定,再用WHEN MATCHED决定更新行为。2.6 存储过程、触发器与异常处理存储过程和触发器是迁移工作里最耗时的一环。MySQL 的存储过程支持DELIMITER作为脚本分隔符,达梦不认这个东西,而且达梦存储过程整体上是 Oracle 风格的块结构。老项目里如果存储过程特别多,建议先做一轮语法扫描,把所有DELIMITER、反引号、IFNULL、NOW()这类共性内容批量替换掉,再进行针对性的逻辑调整。变量声明方面,MySQL 写DECLARE v_count INT DEFAULT 0;,达梦的DECLARE位置和写法基本类似,但在 PL/SQL 块里常写成:CREATE OR REPLACE PROCEDURE test_proc AS v_count INT; BEGIN SELECT COUNT(*) INTO v_count FROM member; IF v_count > 0 THEN DBMS_OUTPUT.PUT_LINE('有数据'); END IF; END;异常处理差异也常踩坑。MySQL 通常用SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'xxx';主动抛错,达梦里可以改成RAISE_APPLICATION_ERROR(-20001, 'xxx');。捕获异常时,MySQL 写EXIT HANDLER FOR SQLEXCEPTION,达梦用EXCEPTION WHEN OTHERS THEN ...结构。如果存储过程里批量使用了这些写法,建议整理成统一模板,再从脚本层面做规则替换。触发器要格外注意新旧值引用。MySQL 用NEW.col和OLD.col,达梦同样支持这样的写法,但在某些场景下需要先声明对应变量再使用,否则直接引用会报“变量未声明”。我习惯的做法是在触发器的BEGIN前先声明:CREATE OR REPLACE TRIGGER trg_member_bi BEFORE INSERT ON member FOR EACH ROW BEGIN IF :NEW.id IS NULL THEN SELECT NEXT VALUE FOR seq_member_id INTO :NEW.id; END IF; END;注意达梦里NEW前要带冒号,写成:NEW,这和 MySQL 的NEW.不带冒号是有区别的。这个冒号很不起眼,但漏掉就会编译失败。3. 迁移实操全流程:结构、数据、业务三层迁移方案不能只看语法差异,还要有一整套从结构到数据再到业务验证的流程。我习惯把迁移拆成三个层面,逐层推进,每一层都有明确的产出物和验证标准。3.1 第一步:表结构和类型映射表结构迁移是基础,但类型映射比很多人想象得更复杂。MySQL 的类型和达梦类型不是一一对应,需要手动或工具自动做映射。高频映射规则我总结如下:MySQL 类型达梦推荐类型注意事项INT/INT(11)INT长度显示宽度直接去掉BIGINTBIGINT通常可以保持TINYINTSMALLINT或TINYINT达梦确实有 TINYINT,但部分场景建议扩宽VARCHAR(n)VARCHAR(n)确认长度是否超过达梦限制TEXTTEXT/CLOB大字段注意应用类型转换DATETIMETIMESTAMP存储精度范围要回测TIMESTAMPTIMESTAMP注意时区和默认值BOOLEAN/TINYINT(1)BIT或SMALLINTMySQL 的 0/1 语义需确认DECIMAL(m,n)DECIMAL(m,n)精度保留,避免溢出BLOBBLOB迁移后需测试读写建表脚本迁移时,还要把ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='表说明'这部分清理掉。达梦不支持这种 MySQL 特有配置,但不影响表注释,COMMENT可以保留。写一个简单的脚本扫描建表 DDL,删除引擎、字符集、自增列这些共性配置,能显著提高导入效率。3.2 第二步:分批导数据,尽量不要一把梭数据迁移最忌讳的就是用一条大 INSERT 把百万行数据一次性导入目标库。达梦对大数据量事务的处理能力和 MySQL 不一样,一把梭可能导致回滚段膨胀、日志暴涨、导入到一半失败。我从实测中总结的分批策略是:按主键或唯一键的范围切片,每批 5 万到 10 万行,一批一个事务提交。如果表没有主键,可以用ROWNUM或先抽取到临时表再加自增列。批量导入时优先使用达梦的批量装载工具,效率远高于逐条 INSERT;如果条件不允许,至少用 JDBC/驱动层的批量参数,把rewriteBatchedStatements之类的能力用起来。导入完成后必须做数据校验。最基础的是行数对比:SELECT COUNT(*) FROM source_table; SELECT COUNT(*) FROM target_table;行数一致不代表数据一致,建议对关键字段再做一次校验和对比。简单做法是分组统计核对总金额、最大值、最小值,复杂做法是用 MD5 拼接所有字段后做哈希比对。第一批迁移建议直接上哈希比对,宁可在校验上多花时间,也别把数据问题留到业务联调阶段暴露。3.3 第三步:SQL 回归清单和测试用例数据搬完以后,业务能不能跑通才是真正的考验。我建议在迁移前就整理一份 SQL 回归清单,把系统里所有高频 SQL 和关键存储过程列进去,逐条在达梦上执行并记录执行结果和耗时。回归清单至少应该包含这几类:分页查询 SQL(每个业务模块的主列表页)聚合统计 SQL(报表、首页仪表盘)关联查询 SQL(多表 JOIN、连表更新)存在唯一键冲突的写入 SQL所有存储过程、触发器、定时任务有了回归清单,迁移验证就变成一项可量化的操作。执行一遍清单,把报错的 SQL 集中改成达梦语法,改完再执行一遍,直到全部通过。这条清单后续也能当作上线前的验收文档,一举两得。4. 常见问题排查与案例复盘这一部分记录我实际排查过的典型案例。每个案例都包含现象、排查思路和最终解决方案,希望能帮你少走弯路。4.1 典型报错速查表报错信息可能原因处理方向“关键字 LIMIT 附近有语法错误”用了 Oracle 兼容模式改为 ROWNUM 嵌套或 OFFSET FETCH 语法“不是 GROUP BY 表达式”分组字段外的列没加聚合函数补齐聚合或调整分组字段“无效的标识符”列名是保留字,或反引号未清理去掉反引号,必要时改列名“自增列的显式插入被拒绝”对 IDENTITY 列插入显式值改成序列 + NEXT VALUE FOR 方案“字符串截断”VARCHAR 长度不够或 CLOB 转换问题调整字段长度,显式 CAST“LISTAGG 返回 CLOB 导致类型不兼容”版本聚合函数类型差异外层加 CAST 成 VARCHAR“过程编译失败”引用了 MySQL 特有语法检查 DELIMITER、异常处理、NEW 冒号4.2 案例一:分页查询在 Oracle 兼容模式下的连锁反应某管理后台的分页列表迁移到达梦后,前三页都能正常打开,第四页以后直接 500。排查后发现列表 SQL 用的是LIMIT 20 OFFSET 40,前三页的偏移量小,达梦在某种处理下勉强兼容,但一旦 OFFSET 变大就触发了语法解析问题。处理方式是把这个模块的所有分页 SQL 统一改成三层 ROWNUM 结构,并在数据库层面做了一次全量扫描,把所有含LIMIT的代码找出来集中改写。这里想提醒一下:兼容模式对 LIMIT 的支持在不同版本、不同参数下表现不一致,不要把“测试环境能跑”当作结论,最好直接用生产环境的兼容参数来验证。4.3 案例二:GROUP_CONCAT 聚合结果被截断某报表模块用GROUP_CONCAT把用户标签拼接成一个逗号分隔的字符串,在 MySQL 里一直正常。迁到达梦后,改成了LISTAGG,大部分用户数据正常,个别标签特别多的用户,报表程序端突然报字符串截断错误。排查后发现达梦的LISTAGG输出类型偏大,程序端拿到的是 CLOB 而不是 VARCHAR,在持久化到中间表时被截断了。最终处理是在LISTAGG外层套CAST(... AS VARCHAR(4000)),同时跟产品确认了字段长度上限。这个案例给我最大的教训是:函数迁移不能只看“能不能跑出结果”,还要看返回类型和长度边界,特别是聚合函数。4.4 案例三:条件字段隐式转换导致索引失效迁移完成后,某订单查询接口从 MySQL 的 300 毫秒变成了达梦的 8 秒。执行计划一查,发现查询条件里的订单号是字符串,而表里的字段类型是数字型,达梦对隐式类型转换的处理方式和 MySQL 有差别,导致索引失效走了全表扫描。解决方法是调整 SQL,让条件类型和字段类型保持一致,显式写成:SELECT * FROM orders WHERE order_id = CAST('100123456' AS BIGINT);这里想多说一句,MySQL 对隐式转换比较宽容,达梦对类型匹配的一致性要求更高。迁移回归清单里建议把“可能存在隐式类型转换的 SQL”单列一组,用EXPLAIN查看执行计划,把全表扫描的 SQL 挑出来重点优化。5. 写在最后的几个经验材料备齐、工具选好以后,迁移本身并不难,真正消耗时间的是问题复现和边界情况处理。我个人在实际操作中的体会是:语法差异清单要动态维护,每次遇到一个新的报错,就记录到清单里,并在文档里标清楚触发条件和解法。这样无论项目里来多少新人,都能快速上手处理同类问题,而不是反复交学费。另一个小技巧是建立“最小改写样例”。对于拿不准的语法,先在测试库建一张临时表,用最小化 SQL 验证达梦的行为,确认无误后再应用到完整 SQL 上。比如不确定LISTAGG返回类型是否会被应用端影响,就先跑一条最简单的聚合语句看看结果类型,再决定是否要加CAST。这种验证方式快且安全,比直接改大 SQL 后交给业务测试高效得多。迁移本身是一次非常好的技术复盘机会。把 MySQL 里的老 SQL 全部过一遍语法,你会发现自己项目里的 SQL 写得有多随意。达梦正好强制你把这些随意的地方收敛起来,从长远看对代码质量和数据库性能都有正向作用。如果后续还有别的系统要迁,前期整理出来的语法改写模板和回归清单可以直接复用,越到后面迁移速度越快。