在SQL Server上插入更新存储的proc


104

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

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

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

用这种方法的替代方法是先进行选择,然后根据返回的行数进行更新或插入。我认为这种方法效率低下,因为如果要进行更新,则会导致2个选择(第一个显式的select调用和第二个隐式的update调用)。如果proc进行插入操作,则效率没有差异。

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

Answers:


61

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

UPSERT的重要性-来自sqlservercentral.com

对于上述情况下的每次更新,如果我们使用UPSERT而不是EXISTS,我们将从表中删除一个附加读取。不幸的是,对于插入,UPPERT和IF EXISTS方法在表上使用相同数量的读取。因此,仅在有充分正当理由证明附加I / O合理时,才应该检查是否存在。最佳的处理方式是确保对DB的读取很少。

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

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


1
好吧,我认为它至少回答了一个问题。而且我没有添加代码,因为问题中的代码已经适合我。尽管我将其放在事务中,但更新时并未考虑隔离级别。感谢您在回答中指出!
binOr

54

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

快速答案请尝试以下模式。它将在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

1
在您的博客文章中,您可以在存在性检查中使用WITH(updlock,serializable)提示作为结论。但是,读取MSDN的状态为:“ UPDLOCK-指定将获取并保持更新锁,直到事务完成。” 这是否意味着可序列化的提示是多余的,因为无论如何其余事务都将保留更新锁,还是我误会了某些内容?
Dan Def),

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。



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检查和回滚可能是个好主意。


@Munish Goyal,因为在数据库中,多个命令和过程以并行方式运行。然后,其他线程可以在运行更新之后并且在运行插入之前插入一行。
Tomas Tintera 2011年

5

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

如果@@ rowcount = 0且@@ error = 0

否则,如果更新由于某种原因而失败,则它将尝试并随后插入,因为失败的语句上的行数为0


3

UPSERT的忠实拥护者确实减少了要管理的代码。这是我执行此操作的另一种方法:输入参数之一是ID,如果ID为NULL或0,则说明它是INSERT,否则是更新。假设应用程序知道是否有一个ID,那么它将不会在所有情况下都能正常工作,但是如果您这样做,它将把执行减少一半。


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发送的所有数据,并尽可能在内部对其进行修复。


1

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.