SQL Server上的INSERT或UPDATE解决方案


596

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

我通常想更新现有记录,或者如果不存在则插入新记录。

实质上:

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

编写此代码的最佳方式是什么?



26
对于首次遇到此问题的任何人-请确保阅读所有答案及其评论。年龄有时可能会导致误导性信息……
Aaron Bertrand 2014年

1
考虑使用SQL Server 2005中引入的EXCEPT运算符
Tarzan

Answers:


370

不要忘记交易。性能不错,但是简单的(IF EXISTS ..)方法非常危险。
当多个线程尝试执行插入或更新时,您很容易会遇到违反主键的情况。

@Beau Crawford和@Esteban提供的解决方案显示了总体思路,但容易出错。

为了避免死锁和PK违规,您可以使用以下方法:

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

要么

begin tran
   update table with (serializable) set ...
   where key = @key

   if @@rowcount = 0
   begin
      insert into table (key, ...) values (@key,..)
   end
commit tran

问题要求最有效的解决方案,而不是最安全的解决方案。事务在增加流程安全性的同时,也增加了开销。
路加·本尼特

31
这两种方法仍然可能失败。如果两个并发线程在同一行上执行相同操作,则第一个将成功,但是第二个插入将由于主键冲突而失败。事务不能保证即使由于记录存在而导致更新失败,插入操作也会成功。为了确保任何数量的并发事务都能成功,您必须使用锁。
让·文森特

7
@aku是否有任何理由要使用表提示(“ with(xxxx)”)而不是在BEGIN TRAN之前使用“ SET TRANSACTION ISOLATION LEVEL SERIALIZABLE”?
EBarr 2010年

4
@CashCow,最后一个获胜,这就是INSERT或UPDATE应该做的:第一个插入,第二个更新记录。添加锁可以在很短的时间内发生这种情况,从而防止发生错误。
让·文森特

1
我一直认为使用锁定提示是不好的,我们应该让Microsoft Internal Engine指示锁定。这是该规则的明显例外吗?

381

查看我对之前非常相似的问题的详细解答

@Beau Crawford的方法在SQL 2005及更低版本的SQL中是一种好方法,尽管如果您授予rep,则应该由第一个人使用。唯一的问题是,对于插入来说,它仍然是两个IO操作。

MS Sql2008 merge从SQL:2003标准引入:

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操作,但是代码很糟糕:-(


10
@Ian Boyd-是的,这是SQL:2003标准的语法,而不是upsert所有其他数据库提供者都决定支持的语法。该upsert语法是要做到这一点远更好的方式,所以至少是MS应该有太多支持-它不象它在T-SQL中唯一的非标准关键字
基思

1
对其他答案中的锁提示有何评论?(很快就会发现,但是如果是推荐的方法,我建议在答案中添加它)
eglasius 2012年

25
有关如何防止竞争条件导致即使使用语法时也可能发生错误的答案,请参见weblogs.sqlteam.com/dang/archive/2009/01/31/…MERGE
Seph 2012年

5
@Seph真是一个惊喜-微软在那里有些失败:-SI猜测这意味着您需要HOLDLOCK在高并发情况下进行合并操作。
基思2012年

11
确实需要更新此答案以解决Seph的评论,即无HOLDLOCK线程安全的评论。根据链接的文章,MERGE隐式地获取了更新锁,但在插入行之前将其释放,这可能导致竞争条件和插入时违反主键的情况。通过使用HOLDLOCK,锁定将一直保持到插入发生之后。
Triynko 2013年

169

进行UPSERT:

更新MyTable SET FieldA = @ FieldA WHERE Key = @ Key

如果@@ ROWCOUNT = 0
   插入MyTable(FieldA)值(@FieldA)

http://en.wikipedia.org/wiki/Upsert


7
如果您应用了适当的唯一索引约束,则不应发生主键冲突。约束的全部目的是防止发生重复的行。尝试插入多少个线程都没有关系,数据库将根据需要执行序列化以强制执行约束...如果没有,则引擎将一文不值。当然,将其包装在序列化事务中将使它更正确,并且更不容易出现死锁或插入失败的情况。
Triynko 2010年

19
@Triynko,我认为@Sam Saffron的意思是,如果两个以上的线程按正确的顺序进行交织,则sql server将抛出错误,表明发生主键冲突。将其包装在可序列化的事务中是防止上述语句中的错误的正确方法。
EBarr

1
即使您的主键是自动递增的,您所关心的还是表上可能存在的唯一约束。
Seph 2012年

1
数据库应处理主要关键问题。您的意思是,如果更新失败,并且先插入一个其他进程,则插入将失败。在这种情况下,您仍然存在比赛条件。锁定不会改变以下事实:后置条件是尝试写入的进程之一将获得该值。
CashCow 2012年

93

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

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

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

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;

很多人会这样建议:

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;

但这一切都是为了确保您可能需要两次读取表才能找到要更新的行。在第一个示例中,您将只需要查找一次行。(在两种情况下,如果在初始读取中均未找到任何行,则会发生插入。)

其他人会这样建议:

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

但是,如果除了让SQL Server捕获本来可以避免的异常以外的其他任何原因而导致的代价昂贵得多的话,这是有问题的,除非在极少数情况下几乎每个插入都会失败。我在这里证明了很多:


3
从插入/更新许多记录的tem表中插入/更新呢?
user960567 2014年

@ user960567好吧,UPDATE target SET col = tmp.col FROM target INNER JOIN #tmp ON <key clause>; INSERT target(...) SELECT ... FROM #tmp AS t WHERE NOT EXISTS (SELECT 1 FROM target WHERE key = t.key);
Aaron Bertrand

4
超过2年后很好的回复:)
user960567 '16

12
@ user960567抱歉,我并不总是实时捕获评论通知。
亚伦·贝特朗

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

编辑:

las,即使对我自己不利,我也必须承认这样做的解决方案似乎没有更好的选择,因为它们只需一步就可以完成任务。


6
我还是更喜欢这个。upsert似乎更像是通过副作用进行编程,而且我从未见过该初始选择会导致实际数据库中出现性能问题的,很少出现的聚集索引查找。
Eric Z Beard

38

如果要一次UPSERT多个记录,则可以使用ANSI SQL:2003 DML语句MERGE。

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语句


1
在Oracle中,我认为发布MERGE语句锁定表。在SQL * Server中会发生同样的情况吗?
Mike McAllister

13
MERGE容易受到竞争条件的影响(请参阅weblogs.sqlteam.com/dang/archive/2009/01/31/…),除非您使它持有证书锁。另外,请看一下MERGE在SQL Profiler中的性能……我发现它通常比其他解决方案要慢,并且产生的读取更多。
EBarr 2010年

@EBarr-感谢您对锁的链接。我已经更新了答案,以包括建议的锁定提示。
埃里克·韦尔瑙


10

尽管对此评论还为时已晚,但我想使用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;

1
使用MERGE还需要担心其他事情:mssqltips.com/sqlservertip/3074/…–
Aaron Bertrand

8
/*
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

用所需的任何内容替换表和字段名称。注意使用ON条件。然后在DECLARE行上为变量设置适当的值(和类型)。

干杯。


7

您可以使用MERGEStatement,此语句用于插入数据(如果不存在)或更新(如果存在)。

MERGE INTO Employee AS e
using EmployeeUpdate AS eu
ON e.EmployeeID = eu.EmployeeID`

@RamenChef我不明白。WHEN MATCHED子句在哪里?
likejudo

@likejudo我没有写这个;我只修改了它。询问撰写帖子的用户。
RamenChef

5

如果要先执行UPDATE if-no-rows-updated然后执行INSERT路由,请考虑先执行INSERT以防止出现争用情况(假设中间没有DELETE)

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

除了避免出现竞争状况外,如果在大多数情况下该记录将已经存在,则这将导致INSERT失败,从而浪费CPU。

从SQL2008起,最好使用MERGE。


有趣的主意,但语法不正确。SELECT需要一个FROM <table_source>和一个TOP 1(除非所选的table_source只有1行)。
jk7 '16

谢谢。我已将其更改为“不存在”。由于按照O / P测试了“密钥”,因此将只有一个匹配的行(尽管可能需要一个多部分密钥:))
Kristen 2016年

4

这取决于使用模式。必须从整体上了解使用情况,而又不会迷失在细节上。例如,如果创建记录后使用模式更新为99%,则“ UPSERT”是最佳解决方案。

在第一次插入(命中)之后,将所有单条语句更新,而不是ifs或buts。插入中的“ where”条件是必需的,否则它将插入重复项,并且您不想处理锁定。

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

2

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


1
我从不需要使用游标对大型数据集执行此操作。您只需要一个更新以更新匹配的记录,并使用select而不是左连接到表的values子句进行插入。
HLGEM,2009年

1

在每个人都出于直接运行这些存储过程的这些臭名昭著的用户的担心而跳入HOLDLOCK-s之前:-)让我指出,您必须通过设计保证新PK-s的唯一性(身份密钥,Oracle中的序列生成器,唯一索引)外部ID,索引覆盖的查询)。这就是问题的阿尔法和欧米茄。如果您没有该功能,那么宇宙中的任何HOLDLOCK-都不会拯救您,如果您有,那么您不需要在UPDLOCK之外的任何内容(或先使用更新)。

Sproc通常在受严格控制的条件下运行,并且假定调用方是受信任的(中间层)。这意味着,如果一个简单的upsert模式(update + insert或merge)曾经看到过重复的PK,这意味着您的中间层或表设计中存在错误,那么在这种情况下SQL会大吼大叫并拒绝记录是件好事。在这种情况下,放置一个HOLDLOCK等于吃异常,并吸收潜在的错误数据,除了降低性能。

话虽这么说,使用MERGE或UPDATE然后在服务器上使用INSERT更加容易,并且更容易出错,因为您不必记住要先选择添加(UPDLOCK)。另外,如果您要进行小批量的插入/更新,则需要了解您的数据,以便确定交易是否合适。它只是不相关记录的集合,因此其他“包络”交易将是有害的。


1
如果您只是进行更新,然后插入而没有任何锁定或提高隔离度,那么两个用户可以尝试将相同的数据传回(如果两个用户尝试在以下位置提交完全相同的信息,我不会认为这是中间层的错误)同时-很大程度上取决于上下文,不是吗?)。他们都输入更新,两者都返回0行,然后都尝试插入。一个获胜,另一个获胜。人们通常试图避免这种情况。
亚伦·伯特兰

1

如果您先尝试进行更新,然后执行插入操作,那么比赛条件真的重要吗?假设您有两个线程想要为key key设置值:

线程1:值= 1
线程2:值= 2

竞赛条件场景示例

  1. 密钥未定义
  2. 线程1更新失败
  3. 线程2更新失败
  4. 线程1或线程2中的一个恰好插入成功。例如螺纹1
  5. 另一个线程因插入(带有错误重复键)而失败-线程2。

    • 结果:要插入的两个胎面的“第一个”决定了数值。
    • 想要的结果:2个线程中的最后一个写入数据(更新或插入)应该确定值

但; 在多线程环境中,OS调度程序决定线程执行的顺序-在上述场景中,在我们具有这种竞争条件的情况下,正是OS决定了执行顺序。即:从系统的角度说“线程1”或“线程2”是“第一”是错误的。

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

对于实现:如果更新后执行插入导致错误“重复键”,则应将其视为成功。

另外,当然不应假定数据库中的值与您最后写入的值相同。


1

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


11
这是一条评论。在没有任何实际示例代码的情况下,这就像该站点上的许多其他注释一样。
swasheck

很老了,但是一个例子会很好。
Matt McCabe 2015年

0

我曾尝试以下解决方案,当并发请求插入语句时,它对我有用。

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

0

您可以使用此查询。在所有SQL Server版本中均可使用。简单明了。但是您需要使用2个查询。如果不能使用,可以使用

    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

注意:请解释否定答案


我猜缺少锁定?
Zeek2 '19

不乏锁定...我使用“ TRAN”。默认的sql-server事务具有锁定。
维克多·桑切斯

-2

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

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

确保您的键列上存在主键约束。

然后你:

  1. 做更新
  2. 如果更新失败是因为包含键的记录已存在,请执行插入操作。如果更新没有失败,则说明您已完成。

您也可以采用另一种方法,即先插入,如果插入失败,则进行更新。通常,第一种方法更好,因为更新比插入要频繁。


...并且先执行插入操作(知道有时会失败)对于SQL Server来说是昂贵的。sqlperformance.com/2012/08/t-sql-queries/error-handling
Aaron Bertrand

-3

进行if if ... else ...涉及至少进行两个请求(一个进行检查,一个进行操作)。以下方法只需要一个在记录存在的地方,如果需要插入则两个:

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')

-3

我通常会做其他一些发帖人所说的,首先检查它是否存在,然后再执行正确的路径。执行此操作时应记住的一件事是,对于一个路径或另一路径,sql缓存的执行计划可能不是最佳的。我相信最好的方法是调用两个不同的存储过程。

FirstSP:
如果存在
   致电SecondSP(UpdateProc)
其他
   致电ThirdSP(InsertProc)

现在,我不再经常听取自己的建议,所以要加些盐。


这可能与SQL Server的古代版本有关,但现代版本具有语句级编译。叉等不是问题,反正这些问题使用单独的程序也无法解决在更新和插入之间做出选择时固有的任何问题……
Aaron Bertrand 2014年

-10

进行选择,如果得到结果,则对其进行更新,否则,创建它。


3
那是对数据库的两次调用。
克里斯·库德莫

3
我认为没有问题。
克林特·埃克

10
这是对数据库的两次调用,最终使与数据库的往返次数增加了一倍。如果应用程序通过大量插入/更新命中数据库,则会损害性能。UPSERT是更好的策略。
Kev

5
它也创造了比赛条件吗?
niico
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.