SQL Server上INSERT或UPDATE的解决方案

Solutions for INSERT OR UPDATE on SQL Server

假设表结构为MyTable(KEY, datafield1, datafield2...)

通常我想更新一个现有记录,或者插入一个不存在的新记录。

基本上:

1
2
3
4
IF (KEY EXISTS)
  run UPDATE command
ELSE
  run INSERT command

写这个最好的方法是什么?


请看我对前面一个非常相似问题的详细回答。

@BeauCrawford在2005及更低版本的SQL中是一个很好的方法,但是如果您要授予rep,它应该是第一个这样做的人。唯一的问题是对于插入,它仍然是两个IO操作。

MS SQL2008从SQL:2003标准引入了merge

1
2
3
4
5
6
7
8
9
10
11
12
MERGE tablename WITH(HOLDLOCK) AS target
USING (VALUES ('new value', 'different value'))
    AS SOURCE (field1, field2)
    ON target.idfield = 7
WHEN matched THEN
    UPDATE
    SET field1 = SOURCE.field1,
        field2 = SOURCE.field2,
        ...
WHEN NOT matched THEN
    INSERT ( idfield, field1, field2, ... )
    VALUES ( 7,  SOURCE.field1, SOURCE.field2, ... )

现在它实际上只是一个IO操作,但是糟糕的代码:-(


不要忘记交易。性能很好,但简单(如果存在….)的方法是非常危险的。当多个线程尝试执行插入或更新时,您可以轻松地获取主键冲突。

@beau crawford&;@esteban提供的解决方案显示了一般的想法,但容易出错。

为了避免死锁和pk冲突,可以使用如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
BEGIN tran
IF EXISTS (SELECT * FROM TABLE WITH (updlock,serializable) WHERE KEY = @KEY)
BEGIN
   UPDATE TABLE SET ...
   WHERE KEY = @KEY
END
ELSE
BEGIN
   INSERT INTO TABLE (KEY, ...)
   VALUES (@KEY, ...)
END
commit tran

1
2
3
4
5
6
7
8
9
BEGIN tran
   UPDATE TABLE WITH (serializable) SET ...
   WHERE KEY = @KEY

   IF @@rowcount = 0
   BEGIN
      INSERT INTO TABLE (KEY, ...) VALUES (@KEY,..)
   END
commit tran


做一个高潮:

1
2
3
4
UPDATE MyTable SET FieldA=@FieldA WHERE KEY=@KEY

IF @@ROWCOUNT = 0
   INSERT INTO MyTable (FieldA) VALUES (@FieldA)

http://en.wikipedia.org/wiki/upsert网站


很多人会建议你使用MERGE,但我警告你不要这样做。默认情况下,它不会保护您不受并发性和竞争条件的影响,而不仅仅是多个语句,但它确实会带来其他危险:

http://www.mssqltips.com/sqlservertip/3074/use-caution-with-sql-servers-merge-statement/

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

1
2
3
4
5
6
7
8
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
UPDATE dbo.table SET ... WHERE PK = @PK;
IF @@ROWCOUNT = 0
BEGIN
  INSERT dbo.table(PK, ...) SELECT @PK, ...;
END
COMMIT TRANSACTION;

很多人会这样建议:

1
2
3
4
5
6
7
8
9
10
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
IF EXISTS (SELECT 1 FROM dbo.table WHERE PK = @PK)
BEGIN
  UPDATE ...
END
ELSE
  INSERT ...
END
COMMIT TRANSACTION;

但所有这些完成的工作都是确保您可能需要读取表两次,才能找到要更新的行。在第一个示例中,您只需要定位行一次。(在这两种情况下,如果在初始读取中找不到行,则会发生插入。)

其他人会这样建议:

1
2
3
4
5
6
7
BEGIN TRY
  INSERT ...
END TRY
BEGIN CATCH
  IF ERROR_NUMBER() = 2627
    UPDATE ...
END CATCH

但是,如果除了让SQL Server捕获本来可以防止的异常之外,其他任何原因都不会导致这种情况的发生,那么这就有问题了,除非在几乎每个插入都失败的罕见情况下。我在这里也证明了:

  • http://www.mssqltips.com/sqlservertip/2632/checking-for-potential-constraint-violations-before-entering-sql-server-try-and-catch-logic/
  • http://www.sqlpperformance.com/2012/08/t-sql-queries/error-handling


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

编辑:

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


如果一次要升迁多个记录,可以使用ansisql:2003 dml语句merge。

1
2
3
MERGE INTO TABLE_NAME WITH (HOLDLOCK) USING TABLE_NAME ON (condition)
WHEN MATCHED THEN UPDATE SET column1 = value1 [, column2 = value2 ...]
WHEN NOT MATCHED THEN INSERT (column1 [, column2 ...]) VALUES (value1 [, value2 ...])

签出SQL Server 2005中的模拟合并语句。


虽然现在评论这个还为时已晚,但我想用merge添加一个更完整的示例。

这种insert+update语句通常称为"upsert"语句,可以使用SQL Server中的merge实现。

这里给出了一个很好的例子:http://weblogs.sqlteam.com/dang/archive/2009/01/31/upsert-race-condition-with-merge.aspx

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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;


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
CREATE TABLE ApplicationsDesSocietes (
   id                   INT IDENTITY(0,1)    NOT NULL,
   applicationId        INT                  NOT NULL,
   societeId            INT                  NOT NULL,
   suppression          BIT                  NULL,
   CONSTRAINT PK_APPLICATIONSDESSOCIETES PRIMARY KEY (id)
)
GO
--*/


DECLARE @applicationId INT = 81, @societeId INT = 43, @suppression BIT = 0

MERGE dbo.ApplicationsDesSocietes WITH (HOLDLOCK) AS target
--set the SOURCE table one row
USING (VALUES (@applicationId, @societeId, @suppression))
    AS SOURCE (applicationId, societeId, suppression)
    --here goes the ON join condition
    ON target.applicationId = SOURCE.applicationId AND target.societeId = SOURCE.societeId
WHEN MATCHED THEN
    UPDATE
    --place your list of SET here
    SET target.suppression = SOURCE.suppression
WHEN NOT MATCHED THEN
    --insert a new line with the SOURCE table one row
    INSERT (applicationId, societeId, suppression)
    VALUES (SOURCE.applicationId, SOURCE.societeId, SOURCE.suppression);
GO

根据需要替换表名和字段名。注意使用条件。然后为declare行上的变量设置适当的值(和类型)。

干杯。


您可以使用MERGE语句,此语句用于插入不存在的数据或更新不存在的数据。

1
2
3
MERGE INTO Employee AS e
USING EmployeeUpdate AS eu
ON e.EmployeeID = eu.EmployeeID`


如果进行更新,如果没有更新的行,则插入路由,请考虑先执行插入以防止竞争条件(假设没有中间删除)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
INSERT INTO MyTable (KEY, FieldA)
   SELECT @KEY, @FieldA
   WHERE NOT EXISTS
   (
       SELECT *
       FROM  MyTable
       WHERE KEY = @KEY
   )
IF @@ROWCOUNT = 0
BEGIN
   UPDATE MyTable
   SET FieldA=@FieldA
   WHERE KEY=@KEY
   IF @@ROWCOUNT = 0
   ... record was deleted, consider looping TO re-run the INSERT, OR RAISERROR ...
END

除了避免竞争条件外,如果在大多数情况下记录已经存在,那么这将导致插入失败,浪费CPU。

对于SQL2008以后的版本,使用合并可能更可取。


在SQL Server 2008中,可以使用MERGE语句


这取决于使用模式。我们必须了解使用情况的大局,而不要迷失在细节中。例如,如果在创建记录后使用模式为99%更新,则"upsert"是最佳解决方案。

在第一次插入(hit)之后,它将是所有单个语句更新,没有ifs或but。插入的"Where"条件是必需的,否则它将插入重复项,并且您不想处理锁定。

1
2
3
4
5
6
7
8
UPDATE <tableName> SET <field>=@FIELD WHERE KEY=@KEY;

IF @@ROWCOUNT = 0
BEGIN
   INSERT INTO <tableName> (FIELD)
   SELECT @FIELD
   WHERE NOT EXISTS (SELECT * FROM tableName WHERE KEY = @KEY);
END

MS SQL Server 2008引入了merge语句,我认为它是SQL:2003标准的一部分。正如许多人所指出的,处理一行的情况并不是什么大问题,但是在处理大型数据集时,需要一个带有所有性能问题的光标。在处理大型数据集时,合并语句将非常受欢迎。


如果先尝试更新,然后插入,竞争条件真的很重要吗?假设您有两个线程想要为密钥设置值:

螺纹1:值=1螺纹2:值=2

竞赛条件场景示例

  • 未定义键
  • 线程1更新失败
  • 线程2更新失败
  • 只有一个线程1或线程2成功插入。例如线程1
  • 另一个线程因insert(错误为duplicate key)失败-线程2。

    • 结果:要插入的两个踏板中的"第一个"决定了值。
    • 期望结果:写入数据(更新或插入)的两个线程中的最后一个应决定值
  • 但是,在多线程环境中,OS调度程序决定线程执行的顺序——在上面的场景中,我们有这个争用条件,是OS决定了执行的顺序。IE:从系统的角度来说,"线程1"或"线程2"是"第一"是错误的。

    当线程1和线程2的执行时间如此接近时,争用条件的结果并不重要。唯一的要求应该是其中一个线程应该定义结果值。

    对于实现:如果更新后再插入导致错误"duplicate key",则应将其视为成功。

    当然,也不应该假定数据库中的值与上次写入的值相同。


    在所有人都跳到holdlock-s之前,出于对这些直接运行存储过程的不熟悉的用户的恐惧,让我指出您必须通过设计(标识键、Oracle中的序列生成器、外部ID-S的唯一索引、索引覆盖的查询)来确保新的PK-S的唯一性。这就是问题的重点。如果你没有,宇宙中没有holdlock-s可以拯救你,如果你有,那么你在第一次选择时不需要updlock以外的任何东西(或者先使用update)。

    存储过程通常在非常受控的条件下运行,并假定受信任的调用方(中间层)。这意味着,如果一个简单的upsert模式(update+insert或merge)曾经看到重复的pk,这意味着中间层或表设计中有一个bug,那么SQL在这种情况下会大喊一个错误并拒绝该记录,这很好。在这种情况下,放置一个holdlock,除了降低性能外,还等于接受异常和潜在的错误数据。

    尽管如此,在您的服务器上使用合并或更新然后插入更容易,并且不太容易出错,因为您不必记住在第一次选择时添加(updlock)。此外,如果您正在小批量进行插入/更新,则需要知道您的数据,以便决定事务是否合适。它只是一个不相关记录的集合,那么额外的"封装"交易将是有害的。


    我尝试过下面的解决方案,当并发的insert语句请求发生时,它对我有效。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    BEGIN tran
    IF EXISTS (SELECT * FROM TABLE WITH (updlock,serializable) WHERE KEY = @KEY)
    BEGIN
       UPDATE TABLE SET ...
       WHERE KEY = @KEY
    END
    ELSE
    BEGIN
       INSERT TABLE (KEY, ...)
       VALUES (@KEY, ...)
    END
    commit tran

    您可以使用此查询。适用于所有SQL Server版本。简单明了。但您需要使用2个查询。如果不能使用合并,则可以使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
        BEGIN TRAN

        UPDATE TABLE
        SET Id = @ID, Description = @Description
        WHERE Id = @Id

        INSERT INTO TABLE(Id, Description)
        SELECT @Id, @Description
        WHERE NOT EXISTS (SELECT NULL FROM TABLE WHERE Id = @Id)

        COMMIT TRAN

    注:请解释答案否定


    如果使用ADO.NET,则DataAdapter将处理此问题。

    如果你想自己处理,可以这样做:

    确保您的键列上有主键约束。

    然后你:

  • 做更新
  • 如果更新失败,因为具有该键的记录已存在,请执行插入操作。如果更新没有失败,您就完成了。
  • 你也可以反过来做,也就是说,先插入,如果插入失败就更新。通常第一种方法更好,因为更新比插入更频繁。


    我通常按照其他几张海报上所说的,先检查它是否存在,然后再做正确的事情。在执行此操作时,您应该记住的一点是,由SQL缓存的执行计划对于一个或另一个路径可能是非最佳的。我认为最好的方法是调用两个不同的存储过程。

    1
    2
    3
    4
    5
    FirstSP:
    IF EXISTS
       CALL SecondSP (UpdateProc)
    ELSE
       CALL ThirdSP (InsertProc)

    现在,我不经常听从自己的建议,所以吃点盐吧。


    正在执行if exists…否则…至少要做两个请求(一个要检查,一个要采取行动)。以下方法只需要一个记录所在的位置,如果需要插入,则需要两个:

    1
    2
    3
    4
    5
    DECLARE @RowExists bit
    SET @RowExists = 0
    UPDATE MyTable SET DataField1 = 'xxx', @RowExists = 1 WHERE KEY = 123
    IF @RowExists = 0
      INSERT INTO MyTable (KEY, DataField1) VALUES (123, 'xxx')

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