
群里突然有人问到“达梦数据库的 MERGE INTO 跟 MySQL 是不是一样”我第一反应是这俩根本不是一回事。如果你在达梦里按 MySQL 的习惯写INSERT ... ON DUPLICATE KEY UPDATE大概率会碰到语法报错反过来把达梦的 MERGE 语句原封不动丢到 MySQL 执行MySQL 8.0 也会直接告诉你语法不支持。这个差异在国产化改造、跨库迁移的背景下特别容易踩坑。我最近刚好在帮客户做一套老系统从 MySQL 往达梦数据库迁移的适配包含大量日终批量更新逻辑核心就是 MERGE 这类“存在则更新、不存在则插入”的操作。这篇就把达梦数据库和 MySQL 两边 MERGE 的用法、底层逻辑、以及我在实际迁移过程中踩过的坑一次性说清楚。文章适合三类人看正在做 Oracle/MySQL 到达梦数据库迁移的开发者需要在达梦数据库里写数据同步任务的运维或 DBA以及只想搞懂“MERGE 到底怎么用”的数据库初学者。我会尽量把原理讲明白也会给出可以直接抄走的 SQL 模板和排查思路。1. 为什么批量更新场景离不开 MERGE 类语句1.1 逐行 UPDATE 的性能灾难先说一个我遇到过的真实场景。业务表里每天要同步上游系统推送的客户信息单次数据量在 10 万到 50 万行之间。最早这个模块是这么写的先按主键查询这条记录是否存在存在就走 UPDATE不存在就走 INSERT。每个客户一条 SQL跑完一次全量同步大概要 40 分钟而且数据库连接经常被占满锁冲突严重。这类“先查再写”的逻辑本质上就是手动实现 MERGE 的语义但问题在于每行数据都要经历一次网络往返、一次 SQL 解析、一次执行计划生成。10 万行就是 10 万次开销。MERGE INTO 的价值就在于把“判断存在性 更新 插入”合成一条语句提交给数据库由数据库引擎在内部完成匹配和分支批量场景下的效率是完全不同的量级。1.2 从 SQL 标准看 MERGE 的来历MERGE INTO 最早是 SQL:2003 标准引入的后来 Oracle 把它发扬光大在 9i 版本就开始支持语法也是目前市面上最完整的。达梦数据库在设计上就是走 Oracle 兼容路线所以它的 MERGE INTO 语法跟 Oracle 基本一致。MySQL 直到现在都还没有实现标准 MERGE 语法它的等价方案是INSERT ... ON DUPLICATE KEY UPDATE这套语法可以理解为 MySQL 自己的特色方言。所以问题的根源是达梦数据库和 MySQL 面对同一个需求用了两套完全不同的语法实现。不理解这一点迁移代码时就容易拿 A 库的写法去套 B 库然后被各种报错折腾到怀疑人生。1.3 什么场景下不建议用 MERGE不是所有“存在就更新”都得用 MERGE。如果只是单行数据的 upsertMERGE 反而有点重如果是几十行的中小批量逐条 UPDATE 加事务也能接受。MERGE 真正发挥威力的是几千行以上的批量同步场景。另外如果更新逻辑极其复杂每条记录的更新规则依赖大量子查询MERGE 语句会变得很长很难调优这时候我通常建议拆成两个步骤先批量 UPDATE 关联表再 INSERT 缺失数据。工具是拿来解决问题的不是拿来炫技的。2. 达梦数据库的 MERGE INTO 语法拆解2.1 标准语法结构达梦数据库我这边主要用的是 DM8的 MERGE INTO 基本语法如下MERGE INTO 目标表 t USING 源表/子查询 s ON (t.关联字段 s.关联字段) WHEN MATCHED THEN UPDATE SET t.字段1 s.字段1, t.字段2 s.字段2 WHEN NOT MATCHED THEN INSERT (t.字段1, t.字段2) VALUES (s.字段1, s.字段2);这里有几个关键点需要解释目标表要更新或插入数据的表也就是你想要“改动”的那张表。源表数据来源可以是物理表、视图、子查询也可以是 WITH 子句构造的临时结果集。源表的作用是提供一批“期望数据”。ON 条件定义目标表和源表之间怎么匹配。这个条件必须能唯一确定目标行否则达梦会直接报错。WHEN MATCHED匹配上时执行 UPDATE也可以选择什么都不做。WHEN NOT MATCHED没匹配上时执行 INSERT。2.2 一个完整的可执行示例我模拟一个场景有一张T_CUSTOMER客户表每天要从T_CUSTOMER_TMP临时表把最新客户数据合并进去。MERGE INTO T_CUSTOMER t USING T_CUSTOMER_TMP s ON (t.CUST_ID s.CUST_ID) WHEN MATCHED THEN UPDATE SET t.CUST_NAME s.CUST_NAME, t.PHONE s.PHONE, t.UPDATE_TIME SYSDATE WHEN NOT MATCHED THEN INSERT (CUST_ID, CUST_NAME, PHONE, CREATE_TIME) VALUES (s.CUST_ID, s.CUST_NAME, s.PHONE, SYSDATE);执行完这条 SQL临时表里有而客户表里没有的数据会被插入两边都有 CUST_ID 的记录会被更新。整个过程一条语句完成不需要写任何存储过程或者循环。2.3 带 DELETE 子句的高级用法达梦和 Oracle 一样支持在 WHEN MATCHED THEN UPDATE 之后继续追加 DELETE 子句。什么意思呢就是匹配到的行除了更新外还可以根据条件把目标表中的某些行删掉。这个能力在写增量同步任务时特别实用。MERGE INTO T_CUSTOMER t USING T_CUSTOMER_TMP s ON (t.CUST_ID s.CUST_ID) WHEN MATCHED THEN UPDATE SET t.CUST_NAME s.CUST_NAME DELETE WHERE t.STATUS DELETED;上面这条语句的含义是匹配上的行先执行 UPDATE如果 UPDATE 之后行的 STATUS 字段等于 DELETED就把这行从目标表里删掉。注意这个 DELETE WHERE 条件是在 UPDATE 之后评估的也就是基于更新后的值来判断这一点我在刚开始用的时候经常搞混。2.4 达梦对 Oracle 语法的兼容细节达梦数据库的 MERGE INTO 之所以能跟 Oracle 这么像是因为 DM8 的兼容模式默认就支持 Oracle 方言。实际开发中你可能会遇到一个库里有不同 schema有的 schema 是 Oracle 模式建的有的是 MySQL 模式建的。我的经验是MERGE INTO 尽量在 Oracle 兼容模式的 schema 下使用MySQL 兼容模式下的支持程度没有前者稳定。另外达梦的 MERGE 中WHEN MATCHED THEN UPDATE ... WHERE这种带过滤条件的写法也是支持的但其行为跟 Oracle 一致是对源表的每条记录判断 WHERE 条件满足才更新。这个位置如果搞错容易出现“更新了不该更新的行”的错觉建议写完尽量在测试库里先跑一遍核对数量。3. MySQL 的等价实现INSERT ... ON DUPLICATE KEY UPDATE3.1 MySQL 为什么不支持标准 MERGEMySQL 的语法体系一直有自己的路线。早期版本里它用REPLACE INTO来实现“要么替换要么插入”的需求后来为了兼顾更新和插入提供了INSERT ... ON DUPLICATE KEY UPDATE。这套机制依赖表上的主键或唯一索引插入时如果触发了重复键冲突就转为执行 UPDATE 操作。可以这么理解MySQL 选择了在 INSERT 语句里挂一个 UPDATE 分支而不是像 Oracle/达梦那样做一条独立的 MERGE 语句。这套设计在实际使用中够用但有几个明显的限制必须有主键或唯一键否则无法触发冲突它只能处理“存在就更新”的场景没法在一条语句里同时做有条件的删除UPDATE 分支里不能用源表名做引用因为本质上它只有一个表。3.2 基本语法与示例同样的客户同步需求在 MySQL 里这么写INSERT INTO t_customer (cust_id, cust_name, phone, create_time) VALUES (C001, 张三, 13800000001, NOW()), (C002, 李四, 13800000002, NOW()) ON DUPLICATE KEY UPDATE cust_name VALUES(cust_name), phone VALUES(phone), update_time NOW();在 MySQL 8.0.20 之前VALUES()函数是获取本次 INSERT 中指定字段值的标准方式。但 MySQL 官方从 8.0.20 开始标记它为废弃写法推荐使用新的别名语法INSERT INTO t_customer (cust_id, cust_name, phone, create_time) VALUES (C001, 张三, 13800000001, NOW()) AS new -- 给待插入的行起别名 ON DUPLICATE KEY UPDATE cust_name new.cust_name, phone new.phone, update_time NOW();这个新写法注意两点别名放在整个 VALUES 列表之后、ON DUPLICATE KEY UPDATE 之前如果你的 MySQL 版本低于 8.0.19用AS语法可能直接报错建议先查一下版本再决定怎么写。生产环境如果是 5.7老老实实用VALUES()反而更稳妥毕竟老函数虽然标记废弃但短期内不会移除。3.3 换个思路用临时表 双语句实现 MERGE 语义如果业务上确实需要标准 MERGE 里的“匹配更新、不匹配插入”两段逻辑而你又不想依赖 INSERT ... ON DUPLICATE KEY UPDATE 的冲突机制可以选择分两步走-- 第一步更新已存在的数据 UPDATE t_customer t JOIN t_customer_tmp s ON t.cust_id s.cust_id SET t.cust_name s.cust_name, t.phone s.phone, t.update_time NOW(); -- 第二步插入不存在的记录 INSERT INTO t_customer (cust_id, cust_name, phone, create_time) SELECT s.cust_id, s.cust_name, s.phone, NOW() FROM t_customer_tmp s LEFT JOIN t_customer t ON t.cust_id s.cust_id WHERE t.cust_id IS NULL;这种方式在 MySQL 里被广泛使用尤其是处理大数据量时执行计划比逐行判断可控。缺点是两条语句之间如果没有放在同一个事务里中间状态会出现数据不一致所以生产环境里务必用事务包裹保证要么都成功要么都回滚。4. 从客户端工具到 SQL 习惯达梦数据库日常操作的几个要点4.1 Navicat 和 DBeaver 连接达梦的正确姿势迁移工作开始之前先把日常操作工具准备好。Navicat 从 15 版本开始就集成了达梦数据库的驱动新建连接时数据库类型选择“达梦”或“DM”主机填达梦服务器 IP端口默认 5236用户名默认 SYSDBA密码安装时设置。如果列表里找不到达梦选项说明 Navicat 版本太老需要升级或者手动导入达梦的 JDBC 驱动。DBeaver 操作路径稍微绕一点新建连接时在搜索框输入 DM选择 Dm 数据库然后在驱动管理里上传达梦安装目录下的DmJdbcDriver18.jarURL 模板填jdbc:dm://IP:5236测试连接前务必确认达梦数据库的监听端口已经开放很多“连不上”其实都是防火墙挡了 5236 端口。DBeaver 连达梦偶尔会碰到元数据读不出来、看不到表结构的情况解决办法是检查连接属性里的 schema 设置把当前用户对应的 schema 填进去比如 SYSDBA 对应SYSDBA这个 schema。4.2 DM 管理工具的使用与常见的界面问题达梦自带的 DM 管理工具就是那个 DMSQL 开发工具老版本叫 DM Manager我一般只用来做管理操作比如创建表空间、用户、导入导出 DMP 文件。很多人刚打开它找不到对象导航栏其实是因为初始布局里对象树没有展开。菜单栏里选“视图”把“对象导航”或“资源管理器”勾选出来左侧树就出来了。网上说“达梦数据库 dm 管理工具没有对象导航栏”的绝大多数都是这个原因。DMP 文件的导入导出也提一嘴。生产库迁移数据时经常用 DMP 方式在 DM 管理工具里选“导入导出”源文件是 DMP 就选逻辑导入注意字符集和模式名要跟源库一致。对比一下达梦MySQL 的备份恢复更多用 mysqldump 或者直接拷贝数据目录两边的思维模式完全不同别拿 MySQL 的习惯去套达梦。4.3 业务函数迁移的典型案例热搜词里有个“达梦数据库 生成首拼码函数”这其实是很多系统都有的小功能把中文姓名转成拼音首字母。MySQL 里大家喜欢写存储函数遍历字符串通过 HEX 或者字符集映射判断汉字区间取拼音首字母。达梦数据库里同样支持这种函数写法但要注意两个坑一是达梦的 VARCHAR 长度默认按字节计算中文字符占用长度跟 MySQL 不一样函数里如果硬编码长度会截断二是达梦的存储函数内默认不能提交事务所以不要在函数里写 COMMIT。迁移这类函数时最好的做法是先把 MySQL 的函数原样贴到达梦执行看报错再逐行调整一般就是类型转换、字符集、内置函数名这几个地方需要改。5. 实测对比达梦和 MySQL 处理同一批数据的表现5.1 测试环境与数据准备我在自己的测试环境里做了一组对比不是为了跑分而是想看看同样一份数据更新逻辑在两边的写法差异和性能表现。环境如下项目达梦MySQL版本DM8MySQL 8.0.33表结构T_CUSTOMERt_customer数据量基础 20 万行同步 5 万行基础 20 万行同步 5 万行关联字段CUST_ID 主键cust_id 主键测试逻辑都是把 5 万行临时表数据合并进主表其中 3 万行已存在需要更新2 万行需要插入。5.2 两边 SQL 写法对比达梦的写法是标准 MERGEMERGE INTO T_CUSTOMER t USING T_CUSTOMER_TMP s ON (t.CUST_ID s.CUST_ID) WHEN MATCHED THEN UPDATE SET t.CUST_NAME s.CUST_NAME, t.PHONE s.PHONE WHEN NOT MATCHED THEN INSERT (CUST_ID, CUST_NAME, PHONE) VALUES (s.CUST_ID, s.CUST_NAME, s.PHONE);MySQL 的等价写法INSERT INTO t_customer (cust_id, cust_name, phone) SELECT cust_id, cust_name, phone FROM t_customer_tmp ON DUPLICATE KEY UPDATE cust_name VALUES(cust_name), phone VALUES(phone);注意 MySQL 版本用到的INSERT INTO ... SELECT ... ON DUPLICATE KEY UPDATE这种组合非常常见但它的行为是逐行判断唯一键冲突执行计划里会出现一个临时表通常叫temporary。达梦的 MERGE 执行计划则是以关联条件驱动两条语句的计划结构差异很大。5.3 性能与行为差异总结在我的测试数据下达梦执行完 5 万行大约用了 1.8 秒MySQL 大约用了 2.4 秒。这个差距不是绝对的跟服务器的磁盘、内存、表索引状态都有关仅供参考。真正值得注意的是行为差异达梦 MERGE 在 ON 条件命中多行时会直接报错提示“MERGE 语句中找到了多个匹配的行”MySQL 的 ON DUPLICATE 机制不存在这个问题因为它只认唯一索引。达梦 MERGE 一次提交一条 SQLMySQL 的 INSERT ... ON DUPLICATE 在大量数据下如果 binlog 格式是 row 模式会产生很大的 binlog 体积。达梦的下标和字符串函数跟 Oracle 一致比如字符串拼接用||而 MySQL 用CONCATSQL 迁移时这些细节都要逐行改。从结果看达梦数据库的 MERGE INTO 在批量 upsert 场景下是完全能打的而且 SQL 语义更清晰。MySQL 的替代方案也不差但使用前提必须是表上有主键或唯一索引否则 ON DUPLICATE 根本不会触发。6. 迁移过程中的踩坑记录与定位思路6.1 坑一表上没有唯一约束导致 MERGE 报错有次我把 MySQL 的表结构直接建到达梦里建表语句里的主键倒是带过来了但有些业务表用的是联合唯一索引不是主键。MERGE 执行时报了类似“违反唯一约束”或“MERGE 内部错误”的错。定位过程其实不复杂先看 ON 条件的字段在目标表上有没有唯一索引没有就只能重建索引或者把 ON 条件补全到能唯一定位一行。我在项目里定了一条规则达梦里跑 MERGEON 条件必须落到唯一索引或者主键上否则不能用 MERGE。这条规则同样适用于 MySQL 的 ON DUPLICATE KEY UPDATE因为它本身就需要唯一键来触发冲突判断。6.2 坑二字符集与隐式转换导致匹配失败还有一个隐蔽的坑两边的字符集不一致。源表是 UTF-8目标表是 GBKMERGE 关联时如果两边类型完全相同因为字符集不同汉字在底层存的是不同的字节序列ON 条件可能匹配不上导致本应更新的数据被当成新数据插入产生重复记录。这种问题最坑的地方是不报错只是数据不对很难发现。排查办法是分步验证分别查询源表和目标表的同一条记录比对关联字段的实际值用DUMP()函数看底层的字节或者直接对比长度。我后来在迁移规范里要求所有参与 MERGE 关联的字段字符集必须统一而且字段类型长度尽量一致避免隐式转换。MySQL 端还有个经典错误是关联字段一个用varchar一个用intMySQL 会自动把字符串转成数字一旦某个字符串不是纯数字会转成 0匹配就乱了。6.3 坑三UPDATE 子句里的 CASE WHEN 容易踩线达梦的 MERGE 的 UPDATE SET 子句支持 CASE WHEN 表达式可以做条件更新这个我在实际业务里很常用。但要注意CASE WHEN 里的条件是在源表的每一行上独立判断的不是先更新再统一过滤。如果你需要根据目标表当前的值决定是否更新一定要在 WHEN MATCHED 分支里带上过滤条件比如WHEN MATCHED THEN UPDATE SET t.CUST_NAME s.CUST_NAME WHERE t.CUST_NAME ! s.CUST_NAME;这个 WHERE 可以减少不必要的 UPDATE避免频繁触发行版本更新和重做日志。MySQL 的 ON DUPLICATE KEY UPDATE 里没有办法在一条语句层级做这种过滤只能用IF()函数模拟写起来相对绕但也能达到目的。6.4 坑四大批量 MERGE 造成的锁与事务问题刚开始在达梦上跑日终批量任务时我还犯过一个比较低级的错误5 万行的 MERGE 没有开显式事务也没分批结果整个任务跑了 10 多分钟期间目标表被锁住业务查询全部阻塞。后来我把批量数据处理的原则统一成数据量超过 5 万行的 MERGE 要分批执行每批 5000 行每批一个事务提交后短暂缓冲再继续下一批。这样既避免长事务带来的锁问题也方便单批失败后重跑。达梦里控制事务可以用SET AUTOCOMMIT OFF配合COMMIT手动提交。MySQL 同样大批量 ON DUPLICATE 也要考虑执行时间超过innodb_lock_wait_timeout默认 50 秒容易直接报锁等待超时调大这个参数或者分批都是常见解法。6.5 坑五MySQL 的子查询更新限制热搜词里有条是“mysql 中更新子查询”这背后其实是一个很常见的报错MySQL 不允许在 UPDATE 的子查询里直接引用目标表很多人写UPDATE t SET ... WHERE id IN (SELECT id FROM t WHERE ...)会报错提示不能把目标表放进 FROM 子查询。解决办法是包一层派生表或者用 JOIN 替代。放到 MERGE 语境下就是这个意思MySQL 的“更新目标表”能力受限较多达梦的 MERGE 则没有这个问题源表可以很自由地引用目标表。所以如果你在 MySQL 里写过很别扭的临时表绕行方案迁移到达梦后往往可以删掉临时表直接用 MERGE 一步到位这个体感差异我迁移时体会非常明显。7. 几个实用小技巧MERGE 语句调试时强烈建议先在目标表上做一个同结构的临时表把 ON 条件、UPDATE 表达式、INSERT 字段都跑一遍确认匹配数量和插入数量符合预期后再换到真实表执行。临时表可以先清空数据用CREATE TABLE TMP_CUSTOMER AS SELECT * FROM T_CUSTOMER WHERE 10这种方式快速建出来不影响线上数据。看结果的方式是分步执行-- 先看匹配到多少行 SELECT COUNT(*) FROM T_CUSTOMER_TMP s WHERE EXISTS (SELECT 1 FROM T_CUSTOMER t WHERE t.CUST_ID s.CUST_ID); -- 再看需要插入多少行 SELECT COUNT(*) FROM T_CUSTOMER_TMP s WHERE NOT EXISTS (SELECT 1 FROM T_CUSTOMER t WHERE t.CUST_ID s.CUST_ID);这两个数字跟 MERGE 执行完后的实际影响行数对照能快速判断逻辑是否写错。MySQL 那边也可以把这两条 SELECT 先跑一遍确认主键冲突的行数和新增行数再执行 INSERT ... ON DUPLICATE。达梦的 DM 管理工具里有一个执行计划功能可以在 SQL 编辑器里选择“查看执行计划”看 MERGE 语句走的嵌套循环还是哈希连接。数据量大了之后如果执行计划变成全表扫描考虑在 ON 字段上建索引。MySQL 这边则用EXPLAIN看ON DUPLICATE KEY UPDATE的执行计划重点关注 Using temporary 和 Using filesort 标记一旦出现SQL 性能大概率有问题。最后再多说一句关于客户端工具连接达梦数据库的体验。Navicat 和 DBeaver 日常查询没问题但跑大批量 DML 或者看执行计划我还是更习惯用 DM 管理工具。它跟达梦数据库的适配度最高导出导入 DMP 文件、看系统视图、调参数都比第三方工具顺手。如果你需要在达梦里做性能排查记住几个常用的系统视图V$LOCK看锁等待V$SESSIONS看会话V$SQL_HISTORY看历史 SQL这些在 MySQL 里对应的则是performance_schema和sys库两边思路完全不同但都是排查问题的重要入口。我这次迁移做完之后最大的体会是MERGE INTO 本身不难写难的是理解数据库设计哲学上的差异。达梦像 Oracle讲究 SQL 语句语义完整、单条语句能力强MySQL 更务实用限制换简单用主键冲突机制实现 upsert。两边的写法都能解决问题但你不能指望一套 SQL 在两边通吃。适配工作做多了之后我现在看一个新库时第一件事不是看它的文档而是先看它默认的语法兼容模式、支持哪些 SQL 方言再有针对性地写语句这样能少走很多弯路。