107

我编写了一个存储过程,如果记录存在,它将进行更新,否则它将进行插入。它看起来像这样:

update myTable set Col1=@col1, Col2=@col2 where ID=@ID
if @@rowcount = 0
insert into myTable (Col1, Col2) values (@col1, @col2)

我以这种方式编写它的逻辑是,更新将使用 where 子句执行隐式选择,如果返回 0,则插入将发生。

这样做的替代方法是进行选择,然后根据返回的行数进行更新或插入。我认为这是低效的,因为如果您要进行更新,它将导致 2 次选择(第一次显式选择调用和第二次隐式在更新的位置)。如果proc要进行插入,那么效率就没有区别。

我的逻辑在这里合理吗?这是您将插入和更新组合到存储过程中的方式吗?

4

9 回答 9

63

您的假设是正确的,这是执行此操作的最佳方法,称为upsert/merge

UPSERT 的重要性 - 来自 sqlservercentral.com

对于上述案例中的每次更新,如果我们使用 UPSERT 而不是 EXISTS,我们将从表中删除一个额外的读取。不幸的是,对于插入来说,UPSERT 和 IF EXISTS 方法都在表上使用相同数量的读取。因此,只有当有非常正当的理由证明额外的 I/O 是合理的时,才应该检查是否存在。做事的优化方法是确保您在数据库上的读取尽可能少。

最好的策略是尝试更新。如果没有行受更新影响,则插入。在大多数情况下,该行已经存在并且只需要一个 I/O。

编辑:请查看此答案和链接的博客文章,以了解此模式的问题以及如何使其安全工作。

于 2008-08-17T07:22:43.960 回答
55

请阅读我博客上的帖子,了解您可以使用的良好、安全的模式。有很多考虑因素,这个问题的公认答案远非安全。

要快速回答,请尝试以下模式。它可以在 SQL 2000 及更高版本上正常工作。SQL 2005 为您提供了打开其他选项的错误处理,而 SQL 2008 为您提供了一个 MERGE 命令。

begin tran
   update t with (serializable)
   set hitCount = hitCount + 1
   where pk = @id
   if @@rowcount = 0
   begin
      insert t (pk, hitCount)
      values (@id,1)
   end
commit tran
于 2008-10-11T09:04:47.527 回答
10

如果要与 SQL Server 2000/2005 一起使用,则需要将原始代码包含在事务中,以确保数据在并发场景中保持一致。

BEGIN TRANSACTION Upsert
update myTable set Col1=@col1, Col2=@col2 where ID=@ID
if @@rowcount = 0
insert into myTable (Col1, Col2) values (@col1, @col2)
COMMIT TRANSACTION Upsert

这会产生额外的性能成本,但会确保数据完整性。

添加,如前所述,应在可用的情况下使用 MERGE。

于 2008-08-24T20:21:28.530 回答
8

顺便说一下,MERGE 是 SQL Server 2008 中的新功能之一。

于 2008-08-17T07:24:48.200 回答
6

你不仅需要在事务中运行它,它还需要高隔离级别。我实际上默认隔离级别是已提交读,并且此代码需要可序列化。

SET transaction isolation level SERIALIZABLE
BEGIN TRANSACTION Upsert
UPDATE myTable set Col1=@col1, Col2=@col2 where ID=@ID
if @@rowcount = 0
  begin
    INSERT into myTable (ID, Col1, Col2) values (@ID @col1, @col2)
  end
COMMIT TRANSACTION Upsert

也许还添加@@error 检查和回滚可能是个好主意。

于 2008-09-17T07:26:40.640 回答
5

如果您没有在 SQL 2008 中进行合并,则必须将其更改为:

如果@@rowcount = 0 和@@error=0

否则,如果由于某种原因更新失败,那么它将尝试在之后插入,因为失败语句的行数为 0

于 2008-09-02T20:13:39.647 回答
3

UPSERT 的忠实拥护者,真正减少了要管理的代码。这是我这样做的另一种方法:其中一个输入参数是 ID,如果 ID 为 NULL 或 0,则您知道它是一个 INSERT,否则它是一个更新。假设应用程序知道是否有 ID,因此不会在所有情况下都有效,但如果您这样做会将执行次数减半。

于 2008-08-29T21:41:33.417 回答
2

修改后的 Dima Malenko 帖子:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE 

BEGIN TRANSACTION UPSERT 

UPDATE MYTABLE 
SET    COL1 = @col1, 
       COL2 = @col2 
WHERE  ID = @ID 

IF @@rowcount = 0 
  BEGIN 
      INSERT INTO MYTABLE 
                  (ID, 
                   COL1, 
                   COL2) 
      VALUES      (@ID, 
                   @col1, 
                   @col2) 
  END 

IF @@Error > 0 
  BEGIN 
      INSERT INTO MYERRORTABLE 
                  (ID, 
                   COL1, 
                   COL2) 
      VALUES      (@ID, 
                   @col1, 
                   @col2) 
  END 

COMMIT TRANSACTION UPSERT 

您可以捕获错误并将记录发送到失败的插入表。
我需要这样做,因为我们正在获取通过 WSDL 发送的任何数据,并在可能的情况下在内部对其进行修复。

于 2012-05-01T14:27:53.913 回答
1

您的逻辑看起来很合理,但是如果您传入了特定的主键,您可能需要考虑添加一些代码来防止插入。

否则,如果您总是在更新不影响任何记录的情况下进行插入,那么当有人在您“UPSERT”运行之前删除记录时会发生什么?现在您尝试更新的记录不存在,因此它将创建一条记录。这可能不是您要寻找的行为。

于 2008-09-02T13:37:56.477 回答