MERGE语句中子查询作USING源时必须显式去重,否则重复业务键会导致报错;正确做法是用CTE配合ROW_NUMBER()按确定性排序取rn=1的行,并确保ON条件不破坏索引、字段映射严格对齐、NULL处理得当、分批同步加事务控制。

子查询作为USING源时,必须显式去重且禁用非确定性排序
直接把 SELECT * 塞进 USING 子句,只要源数据里有重复业务键(比如两条 email = 'a@b.com' 且 tenant_id = 100),MERGE 就会报错:The MERGE statement attempted to update or delete the same row more than once。这不是警告,是硬性终止。
正确做法是把子查询封装成 CTE,并用 ROW_NUMBER() 主动裁决“哪条该留”:
WITH CleanSource AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY email, tenant_id
ORDER BY updated_at DESC, id DESC
) AS rn
FROM staging_user_import
WHERE status = 'valid'
)
MERGE INTO users AS t
USING CleanSource AS s ON t.email = s.email AND t.tenant_id = s.tenant_id
WHEN MATCHED THEN UPDATE SET ...
WHEN NOT MATCHED THEN INSERT ...- 必须用
PARTITION BY覆盖全部业务唯一键,不能只写主键 -
ORDER BY必须是确定性排序(避免NEWID()或无序TOP N) - 过滤条件(如
WHERE status = 'valid')必须放在 CTE 内部,而非USING外层——否则去重逻辑失效
ON 条件里写函数(如 UPPER(email))会导致索引失效和全表扫描
很多人想“兼容大小写”,在 ON 里写 UPPER(t.email) = UPPER(s.email)。这会让目标表的 email 索引完全失效,执行计划变成 Clustered Index Scan,哪怕目标表只有 1 万行,同步延迟也会从毫秒级跳到秒级。
安全替代方案只有两个:
- 确保源和目标字段值大小写一致(ETL 前统一清洗)
- 在目标表上建计算列索引:
ALTER TABLE users ADD email_upper AS UPPER(email) PERSISTED,再在ON中用t.email_upper = s.email_upper
注意:Oracle 的 UPPER() 在 ON 中同样触发全表扫描;SQL Server 的 COLLATE SQL_Latin1_General_CP1_CI_AS 可用于列级大小写不敏感匹配,但需确认目标列已用该排序规则定义。
WHEN MATCHED 和 WHEN NOT MATCHED 共享同一份子查询结果,字段映射必须严格对齐
子查询返回 5 列,但 INSERT 只写了 4 个 VALUES,或者 UPDATE SET 引用了子查询里不存在的别名字段,都会直接报错。更隐蔽的问题是 NULL 语义错位:比如源子查询中 phone 为 NULL,你写 phone = s.phone,目标记录的 phone 就会被清空——而业务本意可能是“不更新就保留原值”。
关键实操点:
-
INSERT (col1, col2)和VALUES(s.col1, s.col2)的列顺序、数量、类型必须完全一致 -
UPDATE SET中对可能为NULL的字段,显式用ISNULL(s.phone, t.phone)(SQL Server)或NVL(s.phone, t.phone)(Oracle) - 时间戳字段(如
updated_at)不要依赖源子查询值,应在UPDATE SET中直接赋GETDATE()或SYSDATE
批量同步必须加事务控制和分页,不能靠子查询“自动分批”
有人以为把子查询写成 SELECT TOP 1000 * FROM ... 就算分批了——错。SQL Server 的 MERGE 会一次性把整个 USING 结果集载入内存做 JOIN,TOP 只影响最终参与合并的行数,不缓解锁竞争或日志压力。
真正可控的分批方式:
- 用
OFFSET / FETCH配合循环:SELECT ... FROM staging ORDER BY id OFFSET @Offset ROWS FETCH NEXT @BatchSize ROWS ONLY - 每批独立包在
BEGIN TRY / BEGIN CATCH中,捕获后记录ERROR_MESSAGE()和当前@Offset -
@BatchSize初始设为 20,压测后逐步调高;超过 50 行易触发锁升级(KEY→PAGE→TABLE)
子查询本身不解决并发安全问题。高并发写入场景下,ON 条件列必须加 HOLDLOCK 提示(SQL Server)或指定 FOR UPDATE(Oracle),否则可能出现幻读导致漏更新。

















