假设MyTable的表结构(KEY, datafield1, datafield2…)

通常,我希望更新现有记录,或者插入不存在的新记录。

从本质上讲:

IF (key exists)
  run update command
ELSE
  run insert command

最好的写法是什么?


当前回答

许多人会建议您使用MERGE,但我提醒您不要使用它。默认情况下,它不会像多条语句那样保护你不受并发性和竞态条件的影响,它还会带来其他危险:

注意SQL Server的MERGE语句 那么,你想使用MERGE,嗯?

即使使用这种“更简单”的语法,我仍然更喜欢这种方法(为简洁起见,省略了错误处理):

BEGIN TRANSACTION;

UPDATE dbo.table WITH (UPDLOCK, SERIALIZABLE) 
  SET ... WHERE PK = @PK;

IF @@ROWCOUNT = 0
BEGIN
  INSERT dbo.table(PK, ...) SELECT @PK, ...;
END

COMMIT TRANSACTION;

请停止使用此UPSERT反模式

很多人会这样建议:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

BEGIN TRANSACTION;

IF EXISTS (SELECT 1 FROM dbo.table WHERE PK = @PK)
BEGIN
  UPDATE ...
END
ELSE
BEGIN
  INSERT ...
END
COMMIT TRANSACTION;

但是这样做的目的是确保您可能需要读取表两次来定位要更新的行。在第一个示例中,您只需要定位一次行。(在这两种情况下,如果从初始读取中没有找到行,则会发生插入。)

其他人会这样建议:

BEGIN TRY
  INSERT ...
END TRY
BEGIN CATCH
  IF ERROR_NUMBER() = 2627
    UPDATE ...
END CATCH

但是,如果不考虑其他原因,让SQL Server捕获您本来可以在第一个位置防止的异常代价要高得多,那么这就有问题了,除非在几乎所有插入都失败的罕见情况下。我在这里证明:

在进入TRY/CATCH之前检查潜在的约束违反 不同错误处理技术对性能的影响

其他回答

IF EXISTS (SELECT * FROM [Table] WHERE ID = rowID)
UPDATE [Table] SET propertyOne = propOne, property2 . . .
ELSE
INSERT INTO [Table] (propOne, propTwo . . .)

编辑:

唉,即使对我自己不利,我也必须承认,没有选择的解决方案似乎更好,因为它们少了一步就完成了任务。

MS SQL Server 2008引入了MERGE语句,我认为它是SQL:2003标准的一部分。正如许多人所表明的那样,处理一行情况并不是什么大问题,但当处理大型数据集时,就需要游标,随之而来的是所有性能问题。MERGE语句在处理大型数据集时将非常受欢迎。

虽然现在评论这个有点晚了,但我想添加一个使用MERGE的更完整的示例。

这样的Insert+Update语句通常被称为“Upsert”语句,可以在SQL Server中使用MERGE实现。

这里举了一个很好的例子: http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx

上面还解释了锁定和并发场景。

我将引用同样的内容作为参考:

ALTER PROCEDURE dbo.Merge_Foo2
      @ID int
AS

SET NOCOUNT, XACT_ABORT ON;

MERGE dbo.Foo2 WITH (HOLDLOCK) AS f
USING (SELECT @ID AS ID) AS new_foo
      ON f.ID = new_foo.ID
WHEN MATCHED THEN
    UPDATE
            SET f.UpdateSpid = @@SPID,
            UpdateTime = SYSDATETIME()
WHEN NOT MATCHED THEN
    INSERT
      (
            ID,
            InsertSpid,
            InsertTime
      )
    VALUES
      (
            new_foo.ID,
            @@SPID,
            SYSDATETIME()
      );

RETURN @@ERROR;

假设您想要插入/更新单行,最优的方法是使用SQL Server的REPEATABLE READ事务隔离级别:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION

    IF (EXISTS (SELECT * FROM myTable WHERE key=@key)
        UPDATE myTable SET ...
        WHERE key=@key
    ELSE
        INSERT INTO myTable (key, ...)
        VALUES (@key, ...)

COMMIT TRANSACTION

当当前运行的事务处于打开状态时,此隔离级别将阻止/阻止后续可重复读事务访问同一行(WHERE key=@key)。 另一方面,对另一行的操作不会被阻塞(WHERE key=@key2)。

做一个选择,如果你得到一个结果,更新它,如果没有,创建它。