库存快照通常需要与实时库存保持同步,传统做法是分别执行UPDATE、INSERT和DELETE三条语句,代码重复且逻辑分散。SQL Server 2008开始提供的MERGE语句将这三个动作整合为一条命令,以源表数据为基准对目标表进行匹配,匹配到的行执行更新,未匹配到的源行执行插入,目标表多余的行执行删除。这种声明式同步特别适合库存快照这类需要周期性对齐的场景。

一、MERGE语句的基本语法与执行逻辑
MERGE的核心语法由USING、ON和多个WHEN分支组成。USING后面可以接源表、视图、派生表或者表值函数,ON定义目标表与源表的连接条件。WHEN MATCHED THEN表示两边都存在的行,通常执行UPDATE;WHEN NOT MATCHED BY TARGET THEN表示只存在于源表的行,通常执行INSERT;WHEN NOT MATCHED BY SOURCE THEN表示只存在于目标表的行,通常执行DELETE。每个分支还可以追加AND条件,只对满足业务规则的数据执行对应操作。
执行顺序并不是按书写顺序盲目进行,而是SQL Server优化器根据连接条件和索引生成执行计划。理解这一点很重要,因为MERGE本质上是基于连接的一次数据同步,它的原子性比三条独立DML更强。一个MERGE语句会在内部使用合适的连接算法完成匹配,并且只对目标表产生必要的修改。末尾的分号不是可选项,MERGE语句必须以分号结束,否则会报语法错误。
下面是一个最小化的MERGE语法框架,先不涉及具体业务列:
MERGE INTO dbo.InventorySnapshot AS target
USING dbo.StockCurrent AS source
ON target.ProductID = source.ProductID
WHEN MATCHED THEN
UPDATE SET target.Quantity = source.Quantity
WHEN NOT MATCHED BY TARGET THEN
INSERT (ProductID, Quantity)
VALUES (source.ProductID, source.Quantity)
WHEN NOT MATCHED BY SOURCE THEN
DELETE;
这个框架演示了MERGE语句的分支结构。INTO后面是目标表,USING后面是源数据,ON负责建立一对一或一对多的映射关系。需要注意的是,如果ON条件不能唯一匹配目标表行,比如连接条件出现一对多,SQL Server会抛出错误,提示MERGE语句多次更新同一目标行。因此在库存场景中,ON条件必须使用唯一键,通常是商品编号或库存快照的主键。
二、刷新库存快照的典型场景与完整示例
假设有一张实时库存表dbo.StockCurrent,记录当前各仓库中每个商品的最新数量;另有一张库存快照表dbo.InventorySnapshot,每天或每小时需要与实时库存对齐。快照表除了商品编号和数量,还包含快照生成时间。传统写法的流程是:先找出快照表和实时表都有的商品执行更新;再找出实时表有而快照表没有的商品执行插入;最后删除快照表中已经不在实时库存里的商品。这个过程可以合并为一个MERGE。
实际业务往往不是全量覆盖,例如只希望更新数量发生变化的行,或者只处理指定仓库的数据。使用MERGE时可以在WHEN MATCHED后添加AND source.Quantity <> target.Quantity,这样只有数量不同才更新,减少日志写入和索引维护。插入分支也可以从源表中过滤出有效商品,删除分支则根据业务规则决定是否真正移除,必要时改为软删除或仅更新状态列。
下面给出完整示例,先创建两张表并插入测试数据:
CREATE TABLE dbo.StockCurrent
(
ProductID INT PRIMARY KEY,
ProductName NVARCHAR(50),
Quantity INT NOT NULL
);
CREATE TABLE dbo.InventorySnapshot
(
ProductID INT PRIMARY KEY,
ProductName NVARCHAR(50),
Quantity INT NOT NULL,
SnapshotTime DATETIME NOT NULL DEFAULT GETDATE()
);
INSERT INTO dbo.StockCurrent (ProductID, ProductName, Quantity) VALUES
(1, N'机械键盘', 120),
(2, N'无线鼠标', 300),
(3, N'显示器支架', 45);
INSERT INTO dbo.InventorySnapshot (ProductID, ProductName, Quantity) VALUES
(1, N'机械键盘', 100),
(2, N'无线鼠标', 300),
(4, N'旧款键盘', 10);
执行下面的MERGE语句,可以让快照表与实时库存表保持一致:
MERGE INTO dbo.InventorySnapshot AS target
USING dbo.StockCurrent AS source
ON target.ProductID = source.ProductID
WHEN MATCHED AND target.Quantity <> source.Quantity THEN
UPDATE SET
target.Quantity = source.Quantity,
target.ProductName = source.ProductName,
target.SnapshotTime = GETDATE()
WHEN NOT MATCHED BY TARGET THEN
INSERT (ProductID, ProductName, Quantity, SnapshotTime)
VALUES (source.ProductID, source.ProductName, source.Quantity, GETDATE())
WHEN NOT MATCHED BY SOURCE THEN
DELETE
OUTPUT $action AS ActionType,
ISNULL(inserted.ProductID, deleted.ProductID) AS ProductID,
inserted.Quantity AS NewQuantity,
deleted.Quantity AS OldQuantity;
这个语句执行后,商品编号1的数量从100更新为120,商品编号3被插入,商品编号4被删除。最后的OUTPUT子句可以返回每一行实际发生的操作类型,$action会显示INSERT、UPDATE或DELETE。这在调试和审计时非常有用,尤其是当刷新动作需要记录日志或通知下游系统时,可以直接把OUTPUT结果写入临时表或日志表。
如果快照表包含历史数据,删除操作可能不符合需求,比如商品下架后快照仍要保留记录。这时可以把WHEN NOT MATCHED BY SOURCE THEN DELETE改成UPDATE,将状态字段置为失效。这样既保留了历史快照,又不会误删数据。MERGE语句的灵活性体现在每个分支都可以独立选择动作,不需要为了一个场景重写整段逻辑。
三、并发控制、性能优化与常见限制
MERGE语句在并发环境下需要注意竞态条件。典型的场景是两个会话同时执行基于相同源表的MERGE,都发现某个商品不存在于快照表,于是同时尝试插入,其中一个会因主键冲突失败。要避免这种情况,可以在USING子句中给源表添加WITH (HOLDLOCK)表提示,提升为范围锁,防止另一个会话在匹配和插入之间插入相同键。这种锁提示在MERGE中的用法比单独INSERT更容易被忽略,但它是保证并发安全的常见手段。
性能方面,MERGE的速度取决于ON条件的连接列是否建立索引。目标表的ProductID主键已经具备聚集索引,但源表如果是复杂的子查询或临时表,优化器可能选择全表扫描。建议在使用MERGE前先把源数据放入带索引的临时表,或者确保源查询本身能够高效过滤。对于大规模刷新,单个MERGE语句会产生较多事务日志,可以通过分批处理控制每批行数,比如在源查询中使用WHERE ProductID BETWEEN 1 AND 1000,循环执行多个小批次。
MERGE并非没有限制。目标表的同一行不能被多个源行匹配,否则会报错;目标表上如果有启用触发器,MERGE的每个DML动作都会触发对应的触发器,但作用域内inserted和deleted的含义需要仔细验证。此外,OUTPUT子句不能引用远程表或者某些带有过滤索引的列。理解这些限制能避免在复杂场景中过度依赖MERGE,比如当业务规则要求针对不同来源执行不同策略时,拆分为多条DML加上显式事务反而更清晰。
综合来看,用MERGE刷新库存快照的核心价值在于减少扫描次数、统一同步逻辑并降低中间状态暴露的风险。只要在唯一键匹配、并发锁提示和输出审计上做好设计,它就能替代大部分手工拼接DML的重复工作。
SQL ServerMERGE语句库存快照修改时间:2026-10-03 10:31:56