拓冰建站拓冰建站
首页 / 资讯中心 / 正文

达梦数据库MERGE INTO与MySQL ON DUPLICATE KEY UPDATE数据同步写法详解

最近在项目中同时维护达梦数据库和MySQL两套环境的同学应该都遇到过这类需求一张业务表每天要从临时表、接口表或者Excel导入的数据里做同步目标表里已有的记录要更新不存在的记录要新增。在Oracle体系里恨不得一条MERGE INTO直接解决到了MySQL这边很多从Oracle转过来的熟手第一反应还是写MERGE INTO结果一执行直接报语法错误——因为MySQL压根没有这个语句。这篇就专门把达梦数据库和MySQL在“存在即更新、不存在即插入”这条路上的写法掰开揉碎讲清楚从语法结构到实操案例再到那些文档里不会写的坑一次说透。先明确一下此处的“MERGE”和git merge、idea中回退merge操作没有任何关系别被搜热词带偏了。本文说的MERGE INTO是SQL标准里的一种DML语句在达梦数据库DM8里属于常规操作在MySQL里需要用INSERT ... ON DUPLICATE KEY UPDATE来替代。文章适合三类人看一是从Oracle转达梦、想快点上手的DBA二是要做双数据库适配的应用开发三是准备国产数据库迁移、正在做语法兼容评估的技术负责人。1. 为什么需要MERGE INTO这种“一条SQL搞定更新插入”的写法如果你只是偶尔更新几张表写两三条UPDATE加INSERT确实无所谓。但真实业务中数据同步场景会逼着你不得不考虑MERGE每天几万条增量数据、目标表有可能命中也有可能落空、还要保证同步过程尽量在一个事务里完成。这时候用传统写法先查一遍判断存在再决定更新还是插入不仅代码啰嗦还会引入严重的并发问题。1.1 传统写法的痛点以订单同步为例老写法大概是这样的逻辑先SELECT COUNT(*) FROM 目标表 WHERE 关联键 ?有记录就UPDATE没记录就INSERT。看起来没有任何问题但放到高并发场景里全是隐患。查和写不是原子操作两条会话同时查到“记录不存在”然后同时执行INSERT必然有一个撞主键或唯一索引报错。你当然可以加锁或者包一层事务再配合SELECT ... FOR UPDATE但复杂度和性能开销立刻上来了。还有一个被很多人忽略的点业务代码里的判断逻辑导致每次同步至少两条SQL网络往返次数翻倍。数据量一大整个同步任务的时间就被拉长。而MERGE INTO这种写法把判断和写入合并成一条SQL交给数据库内核去处理无论从性能还是从代码整洁度上看都更合理。1.2 MERGE INTO到底帮你省掉了什么用一句话概括MERGE INTO它把“源数据”和“目标数据”做一次关联匹配匹配上的走UPDATE分支匹配不上的走INSERT分支整个过程对应用来说是透明的。因为匹配条件和写入动作在同一个语句里描述数据库可以基于关联键做优化也避免了前面说的“先查再写”的竞态问题。这里有个容易混淆的概念要强调MERGE INTO并不是单纯为了少写几行代码数据库内核在执行时可能采用哈希连接、排序合并、索引嵌套循环等多种关联策略并且允许多个分支WHEN MATCHED和WHEN NOT MATCHED都在一条语句里完成写入。这比手动写多条语句更可控也更适合放在存储过程或者批处理脚本里反复调用。2. 达梦数据库MERGE INTO语法详解达梦数据库在语法上高度兼容OracleMERGE INTO的自然使用习惯和Oracle几乎一致。不过实际用起来达梦也有一些自己的“小脾气”尤其是版本差异和对象命名规范上不注意就会踩坑。2.1 语法结构与核心参数达梦的标准写法是MERGE INTO 目标表 t USING (SELECT ... FROM 源表 WHERE 过滤条件) s ON (t.关联字段 s.关联字段) WHEN MATCHED THEN UPDATE SET t.字段1 s.字段1, t.字段2 s.字段2 -- 可选DELETE WHERE 条件 WHEN NOT MATCHED THEN INSERT (字段1, 字段2) VALUES (s.字段1, s.字段2);其中目标表是你要更新或插入数据的那张表USING后面可以接表、视图或者一段子查询ON后面是关联条件。匹配上以后可以执行UPDATE还可以在UPDATE后面接DELETE WHERE条件用来做“软删除”或者“老化数据清理”这是MERGE的一个隐藏功能很多人不知道。WHEN MATCHED和WHEN NOT MATCHED两个分支不是必须同时存在。你完全可以只写匹配后更新不写插入分支也可以只写未匹配插入匹配后什么都不做。达梦和Oracle一样要求USING部分必须有数据源如果源数据为空MERGE不会报错但也不会产生任何操作。2.2 实操要点ON条件的几个坑达梦的MERGE INTO里ON条件有几个非常关键的约束我在文档里没看到写得特别明显但实测全部会报错或者产生错误结果第一ON条件中使用的目标表字段不能出现在UPDATE SET的赋值对象里。举个例子如果你用ON (t.id s.id)做关联那UPDATE SET t.id s.id就是非法的系统会提示“不能更新ON条件中指定的列”。这背后的逻辑很好理解因为关联键一旦被更新数据匹配关系就乱了数据库不允许这种自相矛盾的操作。第二ON条件里不建议使用OR连接多个关联关系否则执行计划可能变得非常不可控。达梦的官方文档虽然没有一刀切禁止但在实际使用中OR条件会让优化器很难选择合适的关联算法很可能退化成全表扫描加嵌套循环性能瞬间劣化。建议把所有关联关系全部用AND写清楚每一个维度都精确匹配。第三USING子查询里尽量做一次预过滤。很多时候你不希望把源表全量加载进去比如只需要同步状态为有效的数据那就在子查询里先WHERE一次而不是在ON条件或WHEN MATCHED分支里再做判断。减少参与关联的数据量对执行效率的提升是立竿见影的。3. MySQL实现MERGE语义的替代方案很多从达梦或者Oracle切到MySQL的朋友最不习惯的就是没有MERGE INTO。MySQL官方的替代方案是INSERT ... ON DUPLICATE KEY UPDATE但不少人把它和REPLACE INTO混着用也没搞明白两者区别结果数据被悄悄重置自增主键还莫名跳号。3.1 认识INSERT ... ON DUPLICATE KEY UPDATEMySQL的语法是INSERT INTO 目标表 (关联字段, 字段1, 字段2) VALUES (关联值, 值1, 值2) ON DUPLICATE KEY UPDATE 字段1 VALUES(字段1), 字段2 VALUES(字段2);它的原理很简单只要插入的数据触发了主键冲突或者唯一索引冲突就自动转成UPDATE。如果没有冲突就正常插入。这里面最核心的是“必须依赖主键或唯一索引”这是和达梦MERGE INTO最大的差异点。达梦的ON条件可以自定义任意关联字段即使这些字段没有唯一索引也能执行虽然性能可能不佳但MySQL必须要有唯一性约束兜底否则ON DUPLICATE KEY根本不会触发。另外从MySQL 8.0.20版本开始官方已经弃用了VALUES()函数建议使用别名写法INSERT INTO 目标表 (关联字段, 字段1, 字段2) VALUES (关联值, 值1, 值2) AS new ON DUPLICATE KEY UPDATE 字段1 new.字段1, 字段2 new.字段2;如果还在用旧写法虽然暂时不报错但控制台会刷DEPRECATED警告积累多了还是很烦的。3.2 别用REPLACE INTO来顶替MERGEREPLACE INTO从功能上看也能实现“有则更新、无则插入”但它和MERGE的语义差了十万八千里。REPLACE INTO的执行逻辑是发现唯一键冲突以后先把旧的记录整行删掉再插入一条新记录。这会导致两个副作用一是目标表中未被指定的列全部丢失等于被重置成了默认值二是如果表上有自增主键每次REPLACE都会改变自增值主键跳号非常严重。对于有外键关联或需要保留审计字段的表来说这是不可接受的。我之前就见过有人用REPLACE INTO同步数据结果表里有个create_time字段每跑一次就被重置成当前时间业务方找过来排查费了好大劲才发现是同步脚本用错了语句。所以只要你还想要“更新部分字段、保留其他字段”的效果就得老老实实用INSERT ... ON DUPLICATE KEY UPDATE。3.3 复杂匹配条件下的两条路如果表上只有一个唯一索引INSERT ... ON DUPLICATE KEY UPDATE完全够用。但真实需求往往更复杂例如要根据两个字段联合判断是否存在或者在没有唯一索引的情况下做比对更新。这时候MySQL没有原生的MERGE能力通常有两个替代方向一是给目标表建立联合唯一索引让ON DUPLICATE KEY UPDATE可以匹配多个字段。这个方案最省事但要注意联合唯一索引会影响插入性能而且有些业务场景里“存在性判断”和“唯一性约束”是两回事不能为了写SQL方便而强行加索引。二是用UPDATE JOIN加INSERT组合实现。UPDATE JOIN的语法如下UPDATE 目标表 t JOIN 源表 s ON t.关联字段 s.关联字段 SET t.字段1 s.字段1 WHERE 过滤条件;先执行这条把命中记录更新掉然后再执行一条INSERT ... SELECT ... WHERE NOT EXISTS把没命中的记录插入。这种方式更灵活也不需要额外建索引但代价是两条SQL不能天然地放在一个事务里保证原子性。好处是思路和MERGE接近业务代码里看起来也更直观适合复杂匹配或临时性数据订正场景。4. 完整实战案例从同步需求到SQL落地讲完了理论用两个真实场景把达梦和MySQL的写法完整串一遍。这两个案例都是我实际处理过的任务覆盖了“数据汇总更新”和“双表数据同步”两种最常见的模式照着改表名、改字段就能直接用到项目里。4.1 案例一每日订单汇总表增量刷新达梦业务部门每天凌晨会生成一份前一天的订单汇总数据放在ODS_ORDER_SUMMARY临时表里需要把这批数据合并到正式报表表REPORT_ORDER_DAILY中。正式表里已有历史数据存在相同订单日期的要更新销售额、订单量不存在的要插入。订单日期和店铺ID是联合维度目标表上建有这两列的联合主键。达梦环境下的写法MERGE INTO REPORT_ORDER_DAILY t USING ( SELECT ORDER_DATE, SHOP_ID, SUM(ORDER_AMOUNT) AS AMOUNT, COUNT(1) AS ORDER_COUNT FROM ODS_ORDER_SUMMARY WHERE ORDER_DATE TRUNC(SYSDATE) - 1 GROUP BY ORDER_DATE, SHOP_ID ) s ON (t.ORDER_DATE s.ORDER_DATE AND t.SHOP_ID s.SHOP_ID) WHEN MATCHED THEN UPDATE SET t.AMOUNT s.AMOUNT, t.ORDER_COUNT s.ORDER_COUNT, t.UPDATE_TIME SYSDATE WHEN NOT MATCHED THEN INSERT (ORDER_DATE, SHOP_ID, AMOUNT, ORDER_COUNT, CREATE_TIME) VALUES (s.ORDER_DATE, s.SHOP_ID, s.AMOUNT, s.ORDER_COUNT, SYSDATE);几个细节值得说明。USING子查询里用TRUNC(SYSDATE) - 1卡住了数据范围这样营业金额和订单数的汇总在源端就完成了MERGE拿到的结果集很小执行很快。UPDATE_TIME和CREATE_TIME分开维护符合审计习惯。最关键的达梦执行MERGE的时候如果目标表数据量很大建议在ON条件涉及的字段上建索引否则就算内部优化器再强也扛不住几百万行表全表扫描。4.2 案例二员工表与员工历史表的全量一致性同步达梦另一个场景是两张表字段基本一致目标表EMP_HISTORY保存所有历史变化源表EMPLOYEE保存当前状态。每天需要把源表数据同步到历史表已存在的按工号更新信息新入职的插入记录。这里还要顺手把已经离职的标记为“离职”状态所以用到了DELETE WHERE分支。MERGE INTO EMP_HISTORY h USING (SELECT EMP_NO, EMP_NAME, DEPT_ID, STATUS FROM EMPLOYEE) e ON (h.EMP_NO e.EMP_NO) WHEN MATCHED THEN UPDATE SET h.EMP_NAME e.EMP_NAME, h.DEPT_ID e.DEPT_ID, h.STATUS e.STATUS DELETE WHERE e.STATUS 离职 WHEN NOT MATCHED THEN INSERT (EMP_NO, EMP_NAME, DEPT_ID, STATUS) VALUES (e.EMP_NO, e.EMP_NAME, e.DEPT_ID, e.STATUS);DELETE WHERE的语义要特别注意它删除的是“匹配上且满足删除条件”的目标表记录而不是源表记录。这里的意思是当员工状态变为离职时从历史表里移除对应记录如果业务要求保留离职记录那就不能加这个分支。这个功能在一些需要定期清理归档的同步场景里非常好用但也容易写错一定要在测试环境用小数据集验证完再上生产。4.3 案例三MySQL里复刻同样的汇总刷新场景同样的“每日订单汇总表增量刷新”在MySQL里用ON DUPLICATE KEY UPDATE来实现。前提是REPORT_ORDER_DAILY表的ORDER_DATE和SHOP_ID组合必须有唯一索引否则触发不了更新逻辑。INSERT INTO REPORT_ORDER_DAILY (ORDER_DATE, SHOP_ID, AMOUNT, ORDER_COUNT, CREATE_TIME, UPDATE_TIME) SELECT ORDER_DATE, SHOP_ID, SUM(ORDER_AMOUNT), COUNT(1), NOW(), NOW() FROM ODS_ORDER_SUMMARY WHERE ORDER_DATE DATE_SUB(CURDATE(), INTERVAL 1 DAY) GROUP BY ORDER_DATE, SHOP_ID ON DUPLICATE KEY UPDATE AMOUNT VALUES(AMOUNT), ORDER_COUNT VALUES(ORDER_COUNT), UPDATE_TIME NOW();如果你用的是MySQL 8.0.20以上版本建议把VALUES(AMOUNT)改成别名写法避免弃用警告。另外这个语句执行成功后想要确认到底插入了几条、更新了几条可以借助ROW_COUNT()函数插入一条时返回1更新一条时返回2没有任何变化时返回0。不过要注意这个返回值只有在当前会话里才能拿到如果通过应用层JDBC获取不同驱动返回的affected rows规则会有差异需要根据实际驱动行为做判断不能想当然。4.4 双库语法迁移的通用心法如果你正在做从达梦到MySQL或者反向的SQL迁移建议掌握一套固定的转换套路而不是遇到一条改一条。我的经验是三步走第一步把MERGE INTO拆成“更新”和“插入”两段语义第二步判断目标表有没有唯一性约束有就用ON DUPLICATE KEY UPDATE没有就考虑UPDATE JOIN INSERT SELECT WHERE NOT EXISTS第三步单独验证边界数据尤其是NULL值、空字符串、全量匹配、全量不匹配这四种情况。在迁移中还要留意数据库函数和日期写法的差异。达梦里用SYSDATEMySQL用NOW()或CURDATE()达梦支持TRUNC(SYSDATE)MySQL里要写DATE(NOW())或DATE_FORMAT(NOW(), %Y-%m-%d)。这些函数层面的差别虽然不在MERGE语法本身但在完整SQL里几乎一定会遇到提前替换能省很多麻烦。5. 达梦和MySQL在MERGE场景下的常见报错与排查技巧下面这些坑都是我在实际项目里真金白银踩出来的。整理成速查表大家可以遇到问题直接对照。5.1 达梦侧的高频报错清单报错现象真正原因处理方式不能更新ON条件中指定的列UPDATE SET里包含了ON关联字段调整ON条件字段或者从UPDATE SET中删除该列数据未更新但也没报错USING子查询结果为空或ON条件没匹配上单独执行USING子查询确认数据量性能极差SQL执行几十秒关联字段缺索引或USING部分未做预过滤给ON条件建索引在子查询内提前WHERE提示语法错误或关键字冲突目标表名、字段名是达梦保留字用双引号括起来或重命名字段别名更新结果和预期不符源数据存在多行匹配同一目标行USING子查询里分组去重确保关联键唯一第一条报错是最常见的。有一次同事把ON (t.ID s.ID)写好后在UPDATE SET里又顺手写了t.ID s.ID达梦直接报错他还以为是数据库BUG。其实这就是语法约束理解了设计逻辑就不会再犯。第二条要特别提醒达梦的MERGE在匹配不到数据时不报错、不警告返回的影响行数是0。所以如果同步任务执行完了没有报错但目标表数据没变优先怀疑是不是USING子查询的结果为空或者日期条件没对上。排查时先把USING部分单独拖出来跑一遍看看到底有没有记录比自己瞎猜快得多。5.2 MySQL侧的行为差异与隐蔽坑MySQL的INSERT ... ON DUPLICATE KEY UPDATE有几个坑很容易在毫不知情的情况下引发数据问题。第一个坑是行数返回值的语义。执行一条语句如果有1行插入成功ROW_COUNT()返回1有1行更新成功返回2命中重复键但更新前后的值完全一样返回0。应用层拿这个返回值做监控的时候很多人默认“影响0行就是没数据”结果把“已存在且无需更新”的情况错判成了同步失败这是典型的理解偏差。第二个坑是并发插入时报“死锁”或者“重复键”的概率比普通插入高。原因在于ON DUPLICATE KEY UPDATE在检测到唯一键冲突后会走更新路径但两个并发会话同时在不存在记录的情况下尝试插入仍然可能有一方在唯一索引检查上撞车。解决方案是确保批量插入的任务做串行化或者用INSERT IGNORE这类策略兜底不要完全依赖数据库自动处理。第三个坑是ON DUPLICATE KEY UPDATE不能像达梦那样指定自定义匹配条件它只认唯一性约束。如果业务上要求根据“逻辑删除标记为0”的记录去重而表里的唯一索引包含了逻辑删除字段实现起来就会很别扭。这种情况下建议回到UPDATE JOIN加INSERT SELECT WHERE NOT EXISTS的组合写法语义更清晰。虽然多写几条SQL但每一行都明明白白不容易出幺蛾子。5.3 我的排查习惯和避坑建议无论达梦还是MySQL我处理这类数据同步问题都有一个固定习惯先在测试库用200行以内的小数据集手工验证打印出USING子查询的结果、目标表关联键的匹配情况、更新前后字段值的差异确认无误后再放大到全量数据。很多人觉得这个步骤浪费时间直接拿生产数据跑一跑就是一个小时报错了还得回滚来回折腾的成本远高于小数据验证。另外建议所有MERGE和ON DUPLICATE KEY UPDATE的SQL都放到事务里执行尤其是批量同步。达梦和MySQL在默认配置下如果一条SQL执行失败已执行的部分有可能部分提交造成数据半新半旧。包一层事务后任何异常都可以整体回滚恢复到执行前的状态。不过要注意MySQL的INSERT ... ON DUPLICATE KEY UPDATE如果涉及大量数据单条SQL持有锁的时间会很长容易拖垮其他业务读写最好分成小批次提交。6. 从工具链看达梦和MySQL的联调体验最后说点工具层面的体会。很多人在达梦上写MERGE INTO不顺手除了语法本身还有一个原因是工具不熟悉。达梦自带的DM管理工具在很多版本里对象导航栏不会主动刷新写完SQL执行后想马上看到目标表数据变化结果导航栏里的表结构还是旧的需要手动刷新或重新连接。这一点和Navicat、DBeaver这类第三方工具连接达梦的体验差不多第三方工具往往更流畅但部分达梦高级特性比如某些管理命令支持度有限。我目前的搭配方案是日常查询用DBeaver数据导入导出用Navicat做达梦数据库管理时用自带的DM管理工具。三套工具各有分工DBeaver胜在轻量和SQL高亮体验好Navicat的导入向导对处理Excel、CSV非常方便DM管理工具则负责执行那些第三方工具不敢保证完全兼容的管理操作。写MERGE INTO的时候我更习惯在DBeaver里先写好并执行因为它的结果集展示和错误提示更直观能直接看到影响行数和错误位置调试体验比DM管理工具舒服不少。要注意的是无论用哪种工具达梦数据库的事务隔离级别和MySQL存在差异工具连接会话的默认设置也可能不同。如果发现MERGE执行结果在工具里看不到先检查是不是事务没有提交。这个坑藏得很深尤其在Navicat里连接达梦时开启自动提交开关和关闭自动提交表现完全是两回事。如果你是在Docker环境里用iserver、Nacos这类中间件连接达梦数据库除了关注数据库本身的MERGE语法还要确认数据库驱动版本是否匹配。驱动太老可能导致新的SQL特性解析失败表现就是同一段SQL在客户端工具里能跑在应用里却报错。这不是SQL的问题是驱动兼容性没跟上。综合来看达梦的MERGE INTO语法完整、性能可控适合在存储过程、定时任务里处理复杂的增量合并逻辑MySQL则要靠INSERT ... ON DUPLICATE KEY UPDATE和其他组合语句来实现类似效果。两边的写法思路可以互相参照但迁移时千万别直接复制SQL。我的个人习惯是每写一条同步语句都顺手在测试库跑一个包含边界数据的小样例把“新增、更新、命中但值不变”三种情况全部覆盖到确认无误再交给业务方验收。这种习惯帮我规避了无数次生产事故也推荐给所有在国产化和传统数据库之间横跳的兄弟们。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门