ChatGPT解决这个技术问题 Extra ChatGPT

SQL Server 上的 INSERT OR UPDATE 解决方案

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

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

本质上:

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

写这个的最佳执行方式是什么?

对于第一次遇到这个问题的人 - 请务必阅读所有答案和他们的评论。年龄有时会导致误导性信息......
考虑使用 SQL Server 2005 中引入的 EXCEPT 运算符。

U
User1

不要忘记交易。性能很好,但简单的(如果存在..)方法非常危险。当多个线程将尝试执行插入或更新时,您很容易得到主键违规。

@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

问题要求最高效的解决方案,而不是最安全的解决方案。虽然事务增加了流程的安全性,但它也增加了开销。
这两种方法仍然可能失败。如果两个并发线程在同一行上做同样的事情,第一个会成功,但第二个插入会因为主键冲突而失败。即使更新失败,事务也不保证插入会成功,因为记录存在。为了保证任何数量的并发事务都会成功,您必须使用锁。
@aku 您在 BEGIN TRAN 之前使用表提示(“with(xxxx)”)而不是“SET TRANSACTION ISOLATION LEVEL SERIALIZABLE”的任何原因?
@CashCow,最后一个获胜,这就是 INSERT 或 UPDATE 应该做的:第一个插入,第二个更新记录。添加锁可以在很短的时间内发生这种情况,从而防止错误。
我一直认为使用锁定提示是不好的,我们应该让微软内部引擎决定锁定。这是该规则的明显例外吗?
C
Community

查看我的detailed answer to a very similar previous question

@Beau Crawford's 在 SQL 2005 及更低版本中是一个好方法,但如果您授予 rep 它应该转到 first guy to SO it。唯一的问题是对于插入它仍然是两个 IO 操作。

MS Sql2008 引入了 SQL:2003 标准中的 merge

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


@Ian Boyd - 是的,这是 SQL:2003 标准的语法,而不是几乎所有其他数据库提供商决定支持的 upsertupsert 语法是一种更好的方法,因此至少 MS 也应该支持它——它不像是 T-SQL 中唯一的非标准关键字
对其他答案中的锁定提示有何评论? (很快就会发现,但如果是推荐的方式,我建议将其添加到答案中)
有关如何防止竞争条件导致即使使用 MERGE 语法也可能发生的错误的答案,请参阅此处 weblogs.sqlteam.com/dang/archive/2009/01/31/…
@Seph 这真是一个惊喜——微软在那里有点失败:-SI 猜测这意味着您需要一个 HOLDLOCK 来在高并发情况下进行合并操作。
这个答案确实需要更新,以解释 Seph 关于它在没有 HOLDLOCK 的情况下不是线程安全的评论。根据链接的帖子,MERGE 隐式取出更新锁,但在插入行之前释放它,这可能导致插入时出现竞争条件和主键违规。通过使用 HOLDLOCK,锁定会一直保留到插入发生之后。
B
Beau Crawford

做一个UPSERT:

UPDATE MyTable SET FieldA=@FieldA WHERE Key=@Key

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

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


如果应用了正确的唯一索引约束,则不应发生主键冲突。约束的重点是防止重复行发生。无论尝试插入多少线程,数据库都会根据需要进行序列化以强制执行约束……如果没有,那么引擎就毫无价值。当然,将它包装在序列化事务中会使它更正确,并且更不容易出现死锁或插入失败。
@Triynko,我认为@Sam Saffron 的意思是,如果两个以上的线程以正确的顺序交错,那么 sql server 将抛出一个错误,表明会发生主键违规。将其包装在可序列化的事务中是防止上述语句集出现错误的正确方法。
即使您有一个自动增量的主键,您也会担心表上可能存在的任何唯一约束。
数据库应该处理主键问题。您的意思是,如果更新失败并且另一个进程首先通过插入到达那里,您的插入将失败。在那种情况下,无论如何你都有竞争条件。锁定不会改变这样一个事实,即后置条件是尝试写入的进程之一将获得该值。
A
Aaron Bertrand

许多人会建议您使用 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 之前检查潜在的约束违规

不同错误处理技术的性能影响


从插入/更新许多记录的 tem 表中插入/更新怎么样?
@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);
2年多后很好回答:)
@user960567 抱歉,我并不总是实时收到评论通知。
@iokevins 没有我能想到的区别。实际上,我在偏好方面被撕裂了,虽然我更喜欢在查询级别使用提示,但当我们谈论时,我更喜欢相反的情况,例如,将 NOLOCK 提示应用于查询中的每个表(在这种情况下,我更喜欢一个 SET 语句稍后修复)。
M
Mitch Wheat
IF EXISTS (SELECT * FROM [Table] WHERE ID = rowID)
UPDATE [Table] SET propertyOne = propOne, property2 . . .
ELSE
INSERT INTO [Table] (propOne, propTwo . . .)

编辑:

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


我还是更喜欢这个。 upsert 似乎更像是通过副作用进行编程,而且我从未见过该初始选择的极少聚集索引搜索会导致真实数据库中的性能问题。
@EricZBeard这与性能无关(尽管它不是总是您正在执行冗余的搜索,具体取决于您检查的内容是否表明重复)。真正的问题是额外操作为竞争条件和死锁打开了机会(我解释了为什么 in this post)。
E
Eric Weilnau

如果您想一次 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 ...])

查看Mimicking MERGE Statement in SQL Server 2005


在 Oracle 中,我认为发出 MERGE 语句会锁定表。 SQL*Server 中是否也会发生同样的情况?
MERGE 容易受到竞争条件的影响(请参阅 weblogs.sqlteam.com/dang/archive/2009/01/31/…),除非您让它持有某些锁。另外,看看 MERGE 在 SQL Profiler 中的性能......我发现它通常比其他解决方案更慢并且产生更多读取。
@EBarr - 感谢锁上的链接。我已更新我的答案以包含建议锁定提示。
u
user243131

尽管对此发表评论已经很晚了,但我想使用 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;

MERGE 还有其他需要担心的事情:mssqltips.com/sqlservertip/3074/…
D
Denver
/*
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 行上的变量设置适当的值(和类型)。

干杯。


S
Saleh Najar

这取决于使用模式。人们必须查看使用大图,而不会迷失在细节中。例如,如果使用模式是在创建记录后 99% 更新,那么“UPSERT”是最佳解决方案。

在第一次插入(命中)之后,将是所有单语句更新,没有 if 或 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

R
RamenChef

您可以使用MERGE语句,该语句用于如果不存在则插入数据或如果存在则更新。

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

@RamenChef 我不明白。 WHEN MATCHED 子句在哪里?
@likejudo 我没有写这个;我只修改了它。询问撰写帖子的用户。
K
Kristen

如果执行 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 和一个 TOP 1(除非选择的 table_source 只有 1 行)。
谢谢。我已将其更改为不存在。由于根据 O/P 对“密钥”进行测试,因此只会有一个匹配的行(尽管这可能需要是一个多部分密钥 :))
b
bjorsig

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


我从来不需要使用游标来处理大型数据集。您只需要更新匹配的记录并使用 select 而不是将连接保留到表的 values 子句插入。
B
Bo Persson

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

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

示例竞争条件场景

未定义键 线程 1 因更新而失败 线程 2 因更新而失败 线程 1 或线程 2 中的一个因插入而成功。例如线程 1 另一个线程因插入失败(带有错误重复键) - 线程 2。结果:要插入的两个线程中的“第一个”决定值。想要的结果:写入数据(更新或插入)的 2 个线程中的最后一个应该决定值

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

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

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

此外,当然永远不要假设数据库中的值与您上次写入的值相同。


Z
ZXX

在每个人因为害怕这些直接运行您的存储过程的邪恶用户而跳到 HOLDLOCK-s 之前 :-) 让我指出您必须通过设计保证新 PK-s 的唯一性(身份密钥、Oracle 中的序列生成器、用于外部 ID-s,索引覆盖的查询)。这就是问题的阿尔法和欧米茄。如果你没有这个,宇宙中的任何 HOLDLOCK-s 都不会拯救你,如果你有,那么你在第一次选择时不需要任何东西(或首先使用更新)。

Sprocs 通常在非常受控的条件下运行,并假设一个受信任的调用者(中间层)。这意味着如果一个简单的 upsert 模式(更新+插入或合并)曾经看到重复的 PK,这意味着您的中间层或表设计中存在错误,并且在这种情况下 SQL 会大喊错误并拒绝记录是很好的。在这种情况下放置 HOLDLOCK 等于吃异常和接收潜在的错误数据,除了降低你的性能。

话虽如此,使用 MERGE 或 UPDATE 然后 INSERT 在您的服务器上更容易并且更不容易出错,因为您不必记住添加 (UPDLOCK) 到第一次选择。此外,如果您正在小批量进行插入/更新,则需要了解您的数据才能确定事务是否合适。它只是一组不相关的记录,那么额外的“封装”交易将是有害的。


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

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

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

V
Victor Sanchez

您可以使用此查询。在所有 SQL Server 版本中工作。这很简单,也很清楚。但是您需要使用 2 个查询。如果不能使用 MERGE 可以使用

    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

注意:请解释答案否定


我猜缺少锁定?
不乏锁定......我使用“TRAN”。默认 sql-server 事务具有锁定。
N
Nenad

假设您要插入/更新单行,最佳方法是使用 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)。


E
Eugene Kaurov

MySQL(以及随后的 SQLite)也支持 REPLACE INTO 语法:

REPLACE INTO MyTable (KEY, datafield1, datafield2) VALUES (5, '123', 'overwrite');

这会自动识别主键并找到要更新的匹配行,如果没有找到则插入新行。

文档:https://dev.mysql.com/doc/refman/8.0/en/replace.html


m
marc_s

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


这是一条评论。在没有任何实际示例代码的情况下,这就像网站上的许多其他评论一样。
很老,但一个例子会很好。
J
Jay

您可以使用:

INSERT INTO tableName (...) VALUES (...) 
ON DUPLICATE KEY 
UPDATE ...

使用这个,如果已经有一个特定键的条目,那么它将更新,否则,它将插入。


L
Luke Bennett

执行 if exists ... 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')

M
Micky McQuade

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

FirstSP:
If Exists
   Call SecondSP (UpdateProc)
Else
   Call ThirdSP (InsertProc)

现在,我不经常听从自己的建议,所以要持保留态度。


这可能与 SQL Server 的古代版本有关,但现代版本具有语句级编译。分叉等不是问题,并且为这些事情使用单独的过程并不能解决在更新和插入之间进行选择所固有的任何问题......
n
nruessmann

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

如果你想自己处理,这是这样的:

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

然后你:

执行更新 如果更新失败,因为键的记录已经存在,执行插入。如果更新没有失败,您就完成了。

你也可以反过来做,即先插入,如果插入失败则进行更新。通常第一种方法更好,因为更新比插入更频繁。


...并且首先进行插入(知道它有时会失败)对于 SQL Server 来说是昂贵的。 sqlperformance.com/2012/08/t-sql-queries/error-handling
C
Clint Ecker

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


这是对数据库的两次调用。
我看不出有什么问题。
问题在于对数据库的两次调用,您最终将到数据库的往返次数增加了一倍。如果应用程序通过大量插入/更新访问数据库,则会损害性能。 UPSERT 是一个更好的策略。
它也创造了一个竞争条件,不是吗?