MERGE是SQL Server增量更新最稳方案,因其原子性避免竞态、主键冲突和脏状态;必须写全语法、ON字段建唯一索引、源数据去重、用CTE/临时表预处理、TOP限流、OUTPUT日志、显式事务。

MERGE 是 SQL Server 存储过程中实现增量更新最稳的手段,不是“能用”,而是“该用且必须用对”。它把 INSERT、UPDATE、DELETE 三步逻辑压缩进一次原子操作,避免竞态、主键冲突和中间脏状态。
为什么不能用 UPDATE + IF NOT EXISTS 拼凑?
常见错误是先 UPDATE,再查 @@ROWCOUNT 判断是否要 INSERT。这在高并发下会出问题:
- 两个会话同时查不到记录,都执行
INSERT→ 主键冲突报错Violation of PRIMARY KEY constraint - 一个会话刚
INSERT完,另一个还在查,结果重复插入 - 事务边界模糊,
OUTPUT或日志捕获难统一
MERGE 的原子性天然规避这些——匹配、更新、插入全在一个执行计划里完成,引擎自己保证不会“看到一半数据”。
SQL Server MERGE 必须写全的语法点
漏掉任意一项,轻则逻辑错,重则直接报错:
-
WHEN NOT MATCHED BY TARGET—— 少了BY TARGET,SQL Server 报错Incorrect syntax near the keyword 'THEN' -
ON条件字段必须有唯一索引(最好是主键或唯一约束),否则可能锁整张表(RangeS-U锁) - 源数据里不能有重复键值,否则报错
The MERGE statement attempted to update or delete the same row more than once,得提前用ROW_NUMBER() OVER (PARTITION BY key_col ORDER BY ...)去重 -
$action是保留变量,返回字符串'INSERT'或'UPDATE',注意是美元符加 action,不是ACTION或action
存储过程里怎么安全封装 MERGE?
别裸写 MERGE,要控制输入、防错、可追溯:
- 源数据用 CTE 或临时表预处理:去重、过滤无效字段、补默认值,比如
ISNULL(source.status, 'active') - 加
TOP (n)限流(尤其批量同步时),避免单次锁表太久,例如MERGE TOP (5000) INTO ... - 用
OUTPUT抓变更明细:OUTPUT $action, inserted.id, deleted.updated_at写入日志表,方便排查“谁动了哪条” - 显式加事务:即使
MERGE自带原子性,外层BEGIN TRAN+TRY/CATCH仍必要,用于捕获死锁、超时等系统级异常
示例片段:
MERGE INTO dbo.orders AS tgt USING (SELECT order_id, amount, status FROM #staging_orders) AS src ON tgt.order_id = src.order_id WHEN MATCHED THEN UPDATE SET amount = src.amount, status = src.status, updated_at = GETDATE() WHEN NOT MATCHED BY TARGET THEN INSERT (order_id, amount, status, created_at) VALUES (src.order_id, src.amount, src.status, GETDATE()) OUTPUT $action, inserted.order_id, deleted.updated_at INTO @merge_log;
跨库或跨服务器场景下的替代方案
如果目标表在 Linked Server 上,MERGE 会失败(SQL Server 不支持远程目标)。此时只能退回到分步逻辑,但必须加强防护:
- 用
OPENQUERY或四部分命名把源数据拉到本地临时表,再对本地表MERGE - 若必须远程写,改用
INSERT ... ON DUPLICATE KEY UPDATE(MySQL)或INSERT ... ON CONFLICT(PostgreSQL),SQL Server 没原生等价语法,得靠IF EXISTS(...) UPDATE ELSE INSERT+ 行级锁(UPDLOCK, HOLDLOCK)模拟 - 注意:远程
JOIN更新性能极差,别图省事直接写UPDATE linked.db.schema.tgt SET ... FROM linked... JOIN local...
真正麻烦的从来不是语法,而是你没意识到 ON 条件没走索引时,MERGE 会悄悄锁住整个目标表几秒甚至几十秒——而这个现象,在开发机上永远测不出来。










