前阵子接了个某内部管理系统的国产化改造任务,要把整套MySQL库迁到达梦数据库(DM8)。数据迁移本身还好,真正让人头疼的是程序里那几百条SQL——MySQL的方言和达梦自带的Oracle风格方言,在语法层面的差异比想象中大得多。这篇文章把我在迁移过程中遇到的高频SQL语法问题、改写方案和整体迁移思路整理出来,希望能帮到正在做类似迁移的人。
1. 先说结论:迁移的真正难点不在数据,而在SQL习惯
很多团队在接到“MySQL迁达梦”这种任务时,第一反应是去看官方迁移工具能搬多少表、多少数据。工具确实能做很多事,但它搬得走表结构、搬得走数据,却搬不走业务代码里那些带有浓郁MySQL风格的SQL写法。
我这次的项目里,数据表结构迁移花了不到一个下午,但程序侧SQL改造和回归测试占了整个迁移周期的一大半时间。原因很简单:数据库引擎换了,SQL方言的底层规则就换了。MySQL的某些“人性化”写法——比如反引号包关键字、LIMIT直接分页、UPDATE JOIN一次改多表——在达梦的默认配置下几乎全是语法错误。只有把这些语法问题逐个摸清楚,迁移才算真正上了路。
1.1 官方迁移工具能搬走什么,搬不走什么
达梦官方生态里有一整套迁移工具,可以把MySQL库里的表结构、索引、约束、视图、存储过程、触发器都拉过来。对于“能自动转换”的对象,比如普通建表语句、常用数据类型映射,工具的处理效果相当不错,能省掉大量体力活。
但它搬不动的东西也很明确:应用系统里手工编写的SQL语句,以及存储过程里MySQL特有的写法。工具转换存储过程时,遇到AUTO_INCREMENT、IFNULL、LIMIT这类关键词,生成的代码常常是半成品,有时甚至直接抛错,需要人一条条去改。所以别指望工具点一下就万事大吉,它充其量把“写代码”变成了“检查工具生成的代码”。
1.2 参数先行:CASE_SENSITIVE和COMPATIBLE_MODE的影响
动手迁移之前,先把达梦数据库几个关键初始化参数定下来,这比改SQL更优先。因为参数不同,语法兼容行为完全是两码事。
第一个是CASE_SENSITIVE,控制标识符大小写是否敏感。MySQL在Windows和Linux下默认对表名大小写敏感程度不同,但SQL关键字和列名一般都不区分大小写;达梦默认是区分大小写的。如果建库时开了大小写敏感,迁移后所有引用双引号标识符的语句都必须严格匹配表名、列名的大小写,否则直接报“无效的列名”或“表或视图不存在”。
第二个是COMPATIBLE_MODE,用于设置语法兼容模式。达梦提供MySQL兼容模式,开启后能支持LIMIT、AUTO_INCREMENT等MySQL常用语法。我建议在项目规划阶段就和DBA确认好这个参数——如果业务系统里有大量MySQL分页写法,能用兼容模式吞掉一部分自然最好;如果确定走Oracle兼容模式,那LIMIT、IFNULL这类写法就得老老实实全部改写。
2. 迁移前一定要摸清的兼容性边界:大小写、保留字和NULL
在动任何SQL之前,先把达梦和MySQL在“标识符规则”“空值语义”上的差别搞明白。这几项属于全局性差异,它们会影响每一条SQL,排查起来特别隐蔽。
2.1 反引号和大小写:第一波翻车现场
MySQL里最顺手的就是反引号包表名和字段名,比如SELECTuserFROMorder。达梦不支持反引号作为标识符引用符,它会把这个反引号当普通字符处理,结果就是直接语法报错。替代方案是改用双引号,但双引号在达梦里又和大小写敏感纠缠在一起:双引号内的标识符会严格按实际大小写匹配。
举个实际例子:MySQL里你写SELECT * FROM USER根本无所谓,表名大小写混乱也能命中;达梦如果用双引号包"USER",那就只能匹配建表时的小写user。说白了,达梦的双引号是“精确锁定”,MySQL的反引号是“柔性模糊”。迁移时我的做法是统一约定:所有标识符在DML语句中去掉引用符,如果必须引用保留字,就用双引号且严格保持和建表语句一致的大小写。
顺带提一下保留字。DM的保留字集合比MySQL大不少,像“USER”“COMMENT”“TYPE”“SIZE”这些词,MySQL用了也就用了,达梦里直接作为表名或列名会报错。迁移结构时工具一般会自动处理,但应用SQL里裸写这些词的就麻烦了,需要加双引号或者改个列名。
2.2 空字符串不是NULL,迁移后统计会变
MySQL有个习惯——插入空字符串时就是'',它和NULL严格区分;但很多业务代码默认“空值也算没填”,于是私底下把空字符串当NULL用。达梦对空字符串和NULL的处理和Oracle类似,在某些函数行为上会把空字符串当成NULL,或者反过来在字符串拼接时产生和MySQL不一致的结果。
我遇到一个非常典型的统计问题:源表某字段存在大量''空字符串,统计时MySQL用COUNT(column)只会统计非NULL行,因为''不为NULL,所以也会被数进去;迁到达梦后,某段使用NVL(column, '未知')的查询,空字符串被转成了NULL,导致结果多出了很多‘未知’项。要避免这类问题,迁移前先跑一遍空值和NULL的分布对比,凡是源表里同时存在''和NULL的字段,都要让业务方确认语义,必要时提前清洗成统一规则,不然上线后数据差异会非常难解释。
3. 六个高频SQL语法问题,逐个给出改写方案
下面这几个语法差异,基本是MySQL迁达梦的“必踩”清单。我按实际遇到频率从高到低排列,每个都给了根因、错误示例和改写示例。
3.1 自增主键:AUTO_INCREMENT和IDENTITY的替换
MySQL建表时最常用的就是id INT AUTO_INCREMENT PRIMARY KEY。达梦在Oracle兼容模式下不支持AUTO_INCREMENT这个关键字,工具转换时可能会生成IDENTITY(1,1),也就是自增列定义。问题往往出在后续的INSERT语句上——很多代码会显式插入id列,或者用SELECT LAST_INSERT_ID()取刚插入的主键值。
达梦里取最新自增值的习惯做法是用IDENTITY()函数,或者通过序列的NEXTVAL/CURRVAL处理。如果表已经用了IDENTITY列,INSERT时又想返回自增值,写法和MySQL完全不同。我的建议是:
- 结构转换时,统一把自增列定义为IDENTITY(1,1),避免业务代码手动维护id。
- 取刚插入主键,MySQL写法SELECT LAST_INSERT_ID(); 改成达梦的写法SELECT IDENT_CURRENT('表名'); 或者利用JDBC的返回主键机制。
- 如果业务里大量使用“先插父表,再拿id插子表”的模式,测试时一定要重点验证并发场景,自增值的隔离性在两种数据库里行为不完全一样。
-- MySQL INSERT INTO authors(name) VALUES('张三'); SELECT LAST_INSERT_ID(); -- DM改写 INSERT INTO authors(name) VALUES('张三'); SELECT IDENT_CURRENT('authors');3.2 日期时间函数:NOW、SYSDATE以及DATEADD的差异
MySQL里拿当前时间习惯写NOW(),SYSDATE(),日期加减习惯写DATE_ADD(create_time, INTERVAL 1 DAY)。达梦在Oracle兼容模式下,SYSDATE可以直接用,但NOW()不一定识别;日期加减的INTERVAL语法也存在兼容问题。
我统一改成如下写法:
-- MySQL SELECT NOW(), DATE_ADD(create_time, INTERVAL 1 DAY) FROM t_order; -- DM改写 SELECT CURRENT_TIMESTAMP, DATEADD(DAY, 1, create_time) FROM t_order;这里有一个容易被带偏的点:达梦也支持INTERVAL表达式,比如create_time + INTERVAL '1' DAY,但和MySQL的INTERVAL 1 DAY写法差异很大,程序里直接替换容易漏掉引号。我建议项目里统一封装日期时间工具函数,避免几百条SQL各写各的。另外,日期格式化函数也不同,MySQL的DATE_FORMAT,达梦里要写成TO_CHAR(create_time, 'YYYY-MM-DD HH24:MI:SS'),格式化符号和MySQL完全不一样,属于另一个高频改写点。
3.3 LIMIT分页和达梦的ROWNUM/TOP写法
MySQL分页最经典:LIMIT 20, 10,表示跳过20行取10行。达梦在MySQL兼容模式下能用LIMIT,但很多生产环境出于整体兼容性考虑,会建库在Oracle兼容模式下,LIMIT这句就直接变成语法错误。
Oracle兼容模式下,达梦分页有两种原生方式。一是用TOP加子查询嵌套,二是用ROWNUM。注意ROWNUM是在排序前或排序后取,取决于SQL书写顺序,这点特别容易踩坑。
-- MySQL SELECT * FROM t_order ORDER BY create_time DESC LIMIT 20, 10; -- DM写法一(TOP + 子查询) SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT * FROM t_order ORDER BY create_time DESC ) t WHERE ROWNUM <= 30 ) WHERE rn > 20; -- DM写法二(如果可用支持) SELECT * FROM t_order ORDER BY create_time DESC LIMIT 20, 10;我的建议是:如果建库时决定开MySQL兼容模式,分页SQL可以原样保留;如果是Oracle兼容模式,集中写一个分页工具类,所有分页查询走公共方法,让不同模式下的差异封闭在一个地方,别散落到业务代码里。
3.4 UPDATE JOIN改写为MERGE
MySQL支持UPDATE t1 JOIN t2 ON t1.id = t2.id SET t1.num = t2.num,一条语句根据关联表批量更新目标表。达梦的Oracle兼容模式不支持这种UPDATE JOIN语法,但支持MERGE INTO,而且MERGE在处理“匹配则更新、不匹配则插入”时更强。
改写时有个关键点:MERGE里的USING子查询要确保关联字段唯一,否则来源表有多行匹配时,达梦会报错或行为不可预期。MySQL的UPDATE JOIN在多行匹配时是取某一行更新,两种数据库的语义有差异,改写后务必验证目标表的最终值。
-- MySQL UPDATE t_order o JOIN t_customer c ON o.customer_id = c.id SET o.customer_name = c.name; -- DM改写 MERGE INTO t_order o USING t_customer c ON (o.customer_id = c.id) WHEN MATCHED THEN UPDATE SET o.customer_name = c.name;3.5 INSERT ... ON DUPLICATE KEY UPDATE的替代思路
MySQL里做“不存在就插入,存在就更新”时,ON DUPLICATE KEY UPDATE几乎是默认解法。达梦不认这个语法,最接近的替代方案也是MERGE INTO。实际项目中我把这类SQL改写成了MERGE,WHEN MATCHED走UPDATE,WHEN NOT MATCHED走INSERT。
需要注意两点:第一,MERGE的目标表和来源表之间要有明确的关联条件,且只更新需要的列,避免把不该动的字段刷掉;第二,如果是批量同步场景,来源表数据量大时MERGE性能比逐条判断好得多,但USING子句里的大查询也要做好索引优化,否则全表扫起来很酸爽。
-- MySQL INSERT INTO t_price(product_id, price) VALUES(101, 99.9) ON DUPLICATE KEY UPDATE price = 99.9; -- DM改写 MERGE INTO t_price tp USING (SELECT 101 AS product_id, 99.9 AS price FROM DUAL) src ON (tp.product_id = src.product_id) WHEN MATCHED THEN UPDATE SET tp.price = src.price WHEN NOT MATCHED THEN INSERT(product_id, price) VALUES(src.product_id, src.price);3.6 GROUP BY的ONLY_FULL_GROUP_BY差异
MySQL在关闭ONLY_FULL_GROUP_BY模式下,SELECT列表里允许出现未参与GROUP BY的列,结果取的是该组任意一行的值。达梦对分组逻辑的校验严格得多,只要SELECT的非聚合列没出现在GROUP BY里或没被聚合函数包裹,直接报“不是GROUP BY表达式”。
这类SQL在MySQL里能跑,迁到达梦后一上线就炸。解决办法是给所有非聚合列加上聚合函数,或者把漏掉的列补进GROUP BY。如果业务语义就是“取组内某条记录的某个字段”,更稳妥的做法是用ROW_NUMBER()等窗口函数按业务规则取特定行,避免靠数据库的宽松行为随机取值。
-- MySQL(宽松模式可运行) SELECT customer_id, order_date, SUM(amount) FROM t_order GROUP BY customer_id; -- DM改写(补列或用聚合) SELECT customer_id, MAX(order_date), SUM(amount) FROM t_order GROUP BY customer_id;4. 迁移方案落地:四步走流程以及每一步的注意事项
语法问题摸清之后,迁移方案本身要有一个可执行的流程。我这次用的是一套相对稳妥的“四步走”:结构迁移、数据迁移、SQL改造、双跑校验。每一步都有独立的验收入口,不会把问题堆到最后。
4.1 结构迁移:先解决表、索引、约束的兼容性
结构迁移用官方迁移工具把表结构、索引、主外键、自增列等对象迁移过去。迁完后别急着导数据,先做一轮结构核对:所有表是否齐全、自增列是否套用了IDENTITY、索引名是否冲突、外键是否有循环依赖。我这次就遇到一个嵌套很深的视图,工具转出来后失效,手工调整了好几层才跑通。
比较省心的做法是导出一个结构DDL脚本,做一次全量review再执行。这一步虽然原始,但能把问题集中在最前面解决。
4.2 数据迁移:分批、校验、断点续传
大表数据迁移建议分批执行,别指望一条INSERT INTO ... SELECT搞定几千万行。官方工具一般支持分批提交,配合行数校验,中途断了还能续。数据搬完后,一定要做“全表行数对比”和“关键字段抽样对比”。
这里要给个提醒:只对比行数远远不够。如果迁移工具遇到某个字段类型转换失败,可能把一个字段置为NULL而不是报错。抽样时要带上业务上比较敏感的字段,比如金额、状态、手机号这些,从源表和目标表各抽一批样本比对内容。
4.3 程序SQL改造:全局扫描加白盒清单
程序侧SQL改造是最耗时的环节。我的做法是先全局扫描代码仓库,把所有SQL语句抽出来,按类型打标签:SELECT、INSERT、UPDATE、DELETE、存储过程。然后结合前面说的兼容性边界,逐类过一遍。
纯手工改不现实,我做了两件事:一是写脚本做关键词扫描,比如搜出所有包含LIMIT、IFNULL、NOW()、ON DUPLICATE、反引号的位置;二是维护一份“SQL改写对照表”,把每个问题文件的改写前后都登记在案,方便出问题时回溯。
4.4 双跑校验与灰度切换
最稳妥的切换方式是新老数据库并行运行一段时间。业务流量先切一部分到达梦,对比同一批请求在两套库里的返回结果和耗时,连续观察异常后才逐步扩大流量。
这个过程里,程序侧的日志一定要加上业务标识和SQL耗时打点,否则出问题都不知道是那一条SQL在慢。我这次在双跑阶段发现了不少隐性问题,包括某些字符串排序规则不一致、字段类型隐式转换导致索引失效等,要不是有对比数据,上线后根本定位不到。
5. 回归验证阶段最容易漏掉的几个细节
双跑和回归阶段,我踩过一些很难查的坑,列出来给大家避雷。
5.1 索引长度和utf8mb4的坑
MySQL常见的utf8mb4编码下一个VARCHAR(255)字段占用字节数不少,建索引时会触发索引长度限制。迁移到达梦后,如果工具把字段映射成更长的VARCHAR类型,索引创建可能直接报“超出索引键长度限制”。我遇到的是老系统里有个VARCHAR(512)的字段需要建索引,在MySQL下用了前缀索引解决,迁移后前缀索引没有被工具正确转换,导致建索引失败。
处理思路:对所有VARCHAR类型的索引列做一次扫描,长字段要么加前缀索引,要么换成CLOB配全文索引,要么干脆缩短业务字段长度。
5.2 存储过程、触发器和隐式转换
迁移工具转存储过程时经常保留原封不动的MySQL语法,需要手工修改。我遇到最多的是三件事:变量声明里的AUTO_INCREMENT、游标写法差异、异常处理语法不一致。
隐式转换的问题更隐蔽。比如MySQL里字符串和数字比较会自动转换,达梦的隐式转换规则更严格,某些场景下不会自动转,导致查询结果不同或索引失效。建议检查一遍所有“字符串字段和数值比较”的条件SQL,显式加上CAST。
5.3 一个实用的回归验证清单
我把这次迁移的检查项整理成了一张表,给团队作为上线前的验证依据:
| 检查项 | 验证方法 | 容易漏掉的坑 |
|---|---|---|
| 表数量与行数 | 全库比对 | 迁移工具静默置空字段导致行数相同但内容不同 |
| 自增列行为 | 连续插入测试 | LAST_INSERT_ID与IDENTITY取值差异 |
| 分页查询边界 | 对比最后一页 | LIMIT和ROWNUM的越界行为不一致 |
| 空字符串统计 | 对比COUNT结果 | 空字符串被当NULL处理后统计值变化 |
| 日期时间精度 | 对比毫秒级数据 | 日期格式化符号不一致导致写入错乱 |
| GROUP BY返回 | 对比随机列取值 | 开合ONLY_FULL_GROUP_BY下行为差异 |
| UPDATE JOIN | 核对关联更新结果 | 多行匹配时MERGE报错 |
| 索引使用情况 | 跑执行计划 | 隐式转换导致索引失效 |
| 触发器/存储过程 | 业务回放测试 | 工具转换不完整 |
| 特殊字符 | 样本数据抽查 | 引号、转义符处理不一致 |
6. 最后聊一下踩坑后的几点体会
做完这个迁移项目,我最大的感受是:换数据库,本质上是在换一套“SQL世界观”。MySQL对开发者很纵容,反引号随便用、LIMIT随手写、隐式转换睁一只眼闭一只眼;达梦在这类规则上明显更严格,因为它要兼顾Oracle生态的语义习惯。越早承认这个差异,越能把改造工作前置,拖到上线前再处理就非常被动。
具体到操作上,我有几点个人经验供参考:
- 先定库参数再审SQL。COMPATIBLE_MODE和CASE_SENSITIVE直接决定你后面要改多少语句,宁可前期多花半天讨论,也不要后期熬夜改代码。
- 建立SQL清单思维。把代码仓库里所有SQL当成可管理的资产,而不是零散字符串,每条改动都有记录,回归时才能说得清改了哪里。
- 双跑阶段多准备几组对比脚本。数据不一致往往是慢慢暴露的,早点发现问题还能从容应对,拖到正式切换后再发现,压力会大很多。
如果让我再做一次类似迁移,我会把“SQL兼容性评估”放在项目启动的第一周,而不是工具迁移之后的第二周。数据库语法差异这件事,评估得越早,返工越少。希望这篇整理能帮你在迁移路上少踩几个坑。