MERGE本身不直接保存历史版本,但可通过WHEN MATCHED分支的UPDATE配合OUTPUT DELETED.*在更新前捕获旧数据并写入历史表,需确保主键精确匹配、避免多行更新冲突,并在事务中加HOLDLOCK等锁保障并发安全。

用 MERGE 实现“更新 + 记录旧版本”的核心逻辑
MERGE 本身不直接支持保存历史版本,但可以利用 MATCHED 分支中的 UPDATE 操作配合 OUTPUT 子句,把被覆盖前的整行(或关键字段)捕获到临时表或历史表中。关键在于:必须在 UPDATE 执行前,通过 OUTPUT DELETED.* 把原数据“抓出来”。
SQL Server 中 MERGE + OUTPUT 的标准写法
SQL Server 是少数原生支持 OUTPUT 与 MERGE 结合的数据库,其他如 PostgreSQL、MySQL 不支持该组合(PostgreSQL 用 RETURNING 配合 UPDATE,MySQL 则需拆成两步)。以下为可直接运行的模式:
INSERT INTO order_history (order_id, status, amount, updated_at, version_type)
SELECT order_id, status, amount, GETDATE(), 'UPDATE'
FROM (
MERGE orders AS tgt
USING (VALUES (@new_id, @new_status, @new_amount))
AS src(order_id, status, amount)
ON tgt.order_id = src.order_id
WHEN MATCHED THEN
UPDATE SET
status = src.status,
amount = src.amount,
updated_at = GETDATE()
OUTPUT DELETED.order_id, DELETED.status, DELETED.amount
) AS old_data;-
DELETED.*只在WHEN MATCHED THEN UPDATE触发时可用,它代表更新前的行 - 不能在
OUTPUT中引用src或tgt别名,只能用INSERTED(更新后)或DELETED(更新前) - 若只记录部分字段(如仅
status变更才记日志),需在外部加WHERE过滤,MERGE内部无法条件性触发OUTPUT
容易踩的坑:事务隔离与并发更新冲突
当多个会话同时对同一行执行 MERGE,可能因丢失更新或死锁导致历史记录不全或报错 Msg 8672(MERGE 语句影响了多行)。必须确保:
- 源数据(
USING子句)按主键/唯一键严格一对一匹配,避免隐式多行匹配 - 在事务中显式加锁:
SELECT ... WITH (UPDLOCK, HOLDLOCK)预查再 MERGE,或直接用WITH (SERIALIZABLE)提升隔离级别 - 不要在
OUTPUT中调用函数(如GETDATE()),它会被多次求值;时间戳应在外部生成后传入
替代方案:为什么有时该放弃 MERGE 改用 UPDATE + INSERT
当历史表结构复杂(如需记录变更字段名、旧值/新值对比)、或目标库不支持 MERGE OUTPUT(如 MySQL),硬套 MERGE 反而增加维护成本。此时更稳妥的做法是:
- 先用
UPDATE ... OUTPUT DELETED.* INTO #tmp_old(SQL Server)或UPDATE ... RETURNING *(PostgreSQL)提取旧数据 - 再用
INSERT INTO history SELECT ..., 'UPDATE' FROM #tmp_old显式写入历史 - MySQL 必须分两步:先
SELECT ... INTO @vars,再UPDATE,最后INSERT—— 且需注意READ COMMITTED下可能读到中间状态
历史版本的完整性取决于“读旧值”和“写新值”是否原子,MERGE 并不天然提供跨表原子性;真正需要强一致性时,得靠事务封装+合理锁策略,而不是依赖 MERGE 语法糖。

















