sql server 的 merge 必须加 with (holdlock) 才能防止高并发下的重复插入,因其非原子操作,需通过 serializable 级别锁确保 on 条件匹配范围独占,且 on 列须为非 null 唯一键、有唯一索引、源数据须去重并预处理 null。

SQL Server 的 MERGE 必须加 WITH (HOLDLOCK) 才能防并发冲突
不加 WITH (HOLDLOCK),MERGE 在高并发下大概率重复插入——它不是“查完再插”原子操作,而是分步执行:先评估 ON 条件,再决定走 WHEN MATCHED 还是 WHEN NOT MATCHED。两个会话同时查到“不存在”,就会都执行 INSERT,触发唯一键冲突。
正确写法是在目标表别名后立即加锁提示:
MERGE INTO users WITH (HOLDLOCK) AS t USING #staging AS s ON t.email = s.email WHEN MATCHED THEN UPDATE SET name = s.name WHEN NOT MATCHED THEN INSERT (email, name) VALUES (s.email, s.name);
-
WITH (HOLDLOCK)等价于WITH (SERIALIZABLE),会把匹配范围锁住,直到事务结束 - 不能写成
FROM users WITH (HOLDLOCK)——MERGE语法不支持FROM子句,必须放在INTO后的目标表上 - 如果目标表有多个唯一键(如
email和phone),只锁email匹配路径,phone冲突仍可能发生,得确保ON覆盖全部业务判重维度
ON 条件必须基于非 NULL 唯一键,且禁止函数包装
哪怕加了 HOLDLOCK,ON 写错照样崩:若匹配列允许 NULL,比如 ON t.email = s.email,而 s.email 是 NULL,整个条件返回 UNKNOWN,所有行都会误入 WHEN NOT MATCHED 分支,导致批量重复插入。
- 务必在源数据预处理阶段过滤掉
NULL:INSERT INTO #staging SELECT email, name FROM @input WHERE email IS NOT NULL - 禁止在
ON中用函数,例如UPPER(t.email) = UPPER(s.email)—— 会跳过索引,锁粒度升为表级,性能断崖下跌,且HOLDLOCK锁住的范围不可控 - 匹配列(如
email)在目标表上必须有唯一索引或主键约束,否则MERGE可能报错The MERGE statement attempted to update or delete the same row more than once
源数据必须去重,否则 MERGE 直接报错
MERGE 对重复源键零容忍:只要 USING 子句输出两行相同 email,就会抛出 The MERGE statement attempted to update or delete the same row more than once,语句中断,事务回滚。
- 不要依赖应用层“保证不重复”,在
USING部分显式去重,推荐用 CTE +ROW_NUMBER():
WITH CleanSrc AS (
SELECT email, name,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY updated_at DESC) rn
FROM #staging
)
MERGE INTO users WITH (HOLDLOCK) AS t
USING (SELECT email, name FROM CleanSrc WHERE rn = 1) AS s
ON t.email = s.email
-- ...
- 如果源是表变量(
@staging),记得在插入前建唯一索引:CREATE UNIQUE INDEX IX_email ON @staging(email),否则ROW_NUMBER()可能无法按预期分区 - 别用
DISTINCT代替ROW_NUMBER()—— 它无法控制“保留哪一条”,业务上常需按时间戳或优先级选最新/最高优记录
存储过程中调用 MERGE 时,事务和错误捕获不能省
单条 MERGE 报错不会自动回滚外层事务;若没包在 BEGIN TRY / BEGIN CATCH 里,一个失败会中断整个存储过程流程,已执行的其他逻辑也无法补偿。
- 每个
MERGE必须独立事务边界:BEGIN TRAN→MERGE→IF @@ERROR = 0 COMMIT ELSE ROLLBACK - 推荐用
TRY/CATCH捕获具体错误,并记录ERROR_MESSAGE()和影响行数:SELECT @RowCount = @@ROWCOUNT, @Error = ERROR_MESSAGE() - 避免在循环里逐行执行
MERGE—— 千行以上性能急剧下降;应先批量载入临时表,再单次MERGE处理全集
真正难的不是写出语法正确的 MERGE,而是让 ON 条件、源数据质量、锁策略和事务控制四者严丝合缝——漏掉任一环,高并发下都可能在凌晨三点弹出唯一键冲突告警。










