在数据库开发场景中,我们经常会遇到这样的需求:当目标表中存在匹配的记录时更新对应字段,不存在时则插入新记录,这种操作被称为Upsert。使用MERGE语句可以在单个SQL语句中完成匹配判断、插入、更新全套逻辑,非常适合放在存储过程中复用。

MERGE语句基础语法
MERGE语句的核心逻辑是将源数据表和目标数据表进行关联匹配,根据匹配结果执行不同的操作。基础语法结构如下:
MERGE 目标表 AS target
USING 源数据表 AS source
ON target.关联字段 = source.关联字段
-- 匹配到记录时执行更新
WHEN MATCHED THEN
UPDATE SET target.字段1 = source.字段1, target.字段2 = source.字段2
-- 未匹配到记录时执行插入
WHEN NOT MATCHED THEN
INSERT (字段1, 字段2, 字段3) VALUES (source.字段1, source.字段2, source.字段3)
-- 可选:未匹配到源数据但目标表存在多余记录时删除
-- WHEN NOT MATCHED BY SOURCE THEN
-- DELETE;
存储过程中实现Upsert的完整示例
假设我们有一个用户积分表user_score,结构如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| user_id | INT | 用户ID,主键 |
| score | INT | 用户积分 |
| update_time | DATETIME | 最后更新时间 |
现在需要编写一个存储过程,传入用户ID和新增积分,实现如果用户已存在则累加积分,不存在则插入新用户积分记录的功能,具体实现如下:
-- 创建存储过程
CREATE PROCEDURE proc_user_score_upsert
@user_id INT, -- 用户ID参数
@add_score INT -- 新增积分参数
AS
BEGIN
-- 设置不返回受影响行数,提升性能
SET NOCOUNT ON;
-- 定义临时表作为源数据,存放本次要处理的用户积分数据
DECLARE @source_table TABLE (
user_id INT,
add_score INT
);
-- 将入参插入临时表,作为MERGE的源数据
INSERT INTO @source_table (user_id, add_score)
VALUES (@user_id, @add_score);
-- 执行MERGE操作
MERGE user_score AS target
USING @source_table AS source
ON target.user_id = source.user_id
-- 匹配到用户时,累加积分并更新时间
WHEN MATCHED THEN
UPDATE SET
target.score = target.score + source.add_score,
target.update_time = GETDATE()
-- 未匹配到用户时,插入新记录
WHEN NOT MATCHED THEN
INSERT (user_id, score, update_time)
VALUES (source.user_id, source.add_score, GETDATE());
SET NOCOUNT OFF;
END
存储过程调用示例
创建完成后,我们可以通过以下方式调用该存储过程:
-- 给用户ID为1001的用户增加50积分,如果用户不存在则插入 EXEC proc_user_score_upsert @user_id = 1001, @add_score = 50; -- 再次调用,此时用户已存在,会累加积分 EXEC proc_user_score_upsert @user_id = 1001, @add_score = 30;
注意事项
- MERGE语句的
ON子句后面的匹配条件必须是唯一匹配,否则如果源数据或目标数据存在重复匹配,会抛出错误。 - 如果不需要删除多余数据,不要添加
WHEN NOT MATCHED BY SOURCE子句,避免误删数据。 - 在存储过程中使用MERGE时,建议添加
SET NOCOUNT ON,减少不必要的网络传输开销。 - 不同数据库对MERGE语句的支持略有差异,上述示例基于SQL Server,MySQL 8.0+也支持类似语法,Oracle的MERGE语法细节稍有不同,需要根据实际数据库调整。
传统方式对比
如果不使用MERGE语句,通常的实现方式是先查询记录是否存在,再决定执行插入还是更新,示例代码如下:
CREATE PROCEDURE proc_user_score_upsert_old
@user_id INT,
@add_score INT
AS
BEGIN
SET NOCOUNT ON;
-- 先查询用户是否存在
IF EXISTS (SELECT 1 FROM user_score WHERE user_id = @user_id)
BEGIN
-- 存在则更新
UPDATE user_score
SET score = score + @add_score, update_time = GETDATE()
WHERE user_id = @user_id;
END
ELSE
BEGIN
-- 不存在则插入
INSERT INTO user_score (user_id, score, update_time)
VALUES (@user_id, @add_score, GETDATE());
END
SET NOCOUNT OFF;
END
这种方式需要两次数据库交互(一次查询、一次写操作),而MERGE语句只需要一次交互,在高并发场景下性能优势更明显,也能避免查询和写入之间的数据不一致问题。