SQL Server 2005 中的原子 UPSERT

Posted

技术标签:

【中文标题】SQL Server 2005 中的原子 UPSERT【英文标题】:Atomic UPSERT in SQL Server 2005 【发布时间】:2011-02-01 02:49:01 【问题描述】:

在 SQL Server 2005 中执行原子“UPSERT”(如果存在则更新,否则插入)的正确模式是什么?

我在 SO 上看到了很多代码(例如,参见 Check if a row exists, otherwise insert),它们具有以下两部分模式:

UPDATE ...
FROM ...
WHERE <condition>
-- race condition risk here
IF @@ROWCOUNT = 0
  INSERT ...

IF (SELECT COUNT(*) FROM ... WHERE <condition>) = 0
  -- race condition risk here
  INSERT ...
ELSE
  UPDATE ...

其中 将是对自然键的评估。上述方法似乎都不能很好地处理并发。如果我不能让两行具有相同的自然键,那么上述所有操作似乎都有在竞争条件场景中插入具有相同自然键的行的风险。

我一直在使用以下方法,但我很惊讶在人们的反应中没有看到它,所以我想知道它有什么问题:

INSERT INTO <table>
SELECT <natural keys>, <other stuff...>
FROM <table>
WHERE NOT EXISTS
   -- race condition risk here?
   ( SELECT 1 FROM <table> WHERE <natural keys> )

UPDATE ...
WHERE <natural keys>

请注意,这里提到的竞争条件与前面代码中的竞争条件不同。在较早的代码中,问题是幻读(行被另一个会话插入到 UPDATE/IF 之间或 SELECT/INSERT 之间)。在上面的代码中,竞争条件与 DELETE 有关。在 (WHERE NOT EXISTS) 执行之后但在 INSERT 执行之前,另一个会话是否可以删除匹配的行?目前尚不清楚 WHERE NOT EXISTS 在何处与 UPDATE 一起锁定任何内容。

这是原子的吗?我无法找到这将在 SQL Server 文档中记录的位置。

编辑:我意识到这可以通过事务来完成,但我认为我需要将事务级别设置为 SERIALIZABLE 以避免幻读问题?对于这样一个常见的问题,这肯定是矫枉过正吗?

【问题讨论】:

Mladen Prajdić 在这里有一篇您可能会感兴趣的文章。 sqlteam.com/article/… 和这里 weblogs.sqlteam.com/mladenp/archive/2007/07/30/60273.aspx 任何请求的正确模式涉及“Atomic”一词和多个SQL语句应该始终与 BEGIN TRANSACTION 和 COMMIT/ROLLBACK 绑定。 【参考方案1】:
INSERT INTO <table>
SELECT <natural keys>, <other stuff...>
FROM <table>
WHERE NOT EXISTS
   -- race condition risk here?
   ( SELECT 1 FROM <table> WHERE <natural keys> )

UPDATE ...
WHERE <natural keys>
在第一个 INSERT 中存在竞争条件。键可能在内部查询 SELECT 期间不存在,但在 INSERT 时确实存在,导致键冲突。 在 INSERT 和 UPDATE 之间存在竞争条件。在 INSERT 的内部查询中检查时,该键可能存在,但在 UPDATE 运行时已消失。

对于第二个竞争条件,人们可能会争辩说该密钥无论如何都会被并发线程删除,因此它并不是真正的丢失更新。

最佳解决方案通常是尝试最可能的情况,并在失败时处理错误(当然是在事务内部):

如果密钥可能丢失,请始终先插入。处理违反唯一约束,回退到更新。 如果密钥可能存在,请始终先更新。如果没有找到行,则插入。处理可能的唯一约束违规,回退到更新。

除了正确性之外,这种模式也是速度的最佳选择:尝试插入和处理异常比进行虚假锁定更有效。锁定意味着逻辑页面读取(可能意味着物理页面读取),IO(甚至逻辑)比 SEH 更昂贵。

更新@Peter

为什么单个语句不是“原子的”?假设我们有一个简单的表:

create table Test (id int primary key);

现在,如果我在一个循环中从两个线程运行这个单一语句,它将是“原子的”,正如你所说,不存在竞争条件:

  insert into Test (id)
    select top (1) id
    from Numbers n
    where not exists (select id from Test where id = n.id); 

然而在几秒钟内,主键违规发生了:

消息 2627,第 14 级,状态 1,第 4 行 违反主键约束“PK__Test__24927208”。无法在对象“dbo.Test”中插入重复键。

这是为什么呢?您是正确的,因为 SQL 查询计划将在 DELETE ... FROM ... JOINWITH cte AS (SELECT...FROM ) DELETE FROM cte 和许多其他情况下执行“正确的事情”。但在这些情况下有一个关键的区别:“子查询”指的是 updatedelete 操作的 target。对于这种情况,查询计划确实会使用适当的锁,事实上我这种行为在某些情况下很关键,比如在实现队列时Using tables as Queues。

但是在原始问题以及我的示例中,查询优化器将子查询视为查询中的子查询,而不是某些需要特殊锁定保护的特殊“扫描更新”类型查询。结果是子查询查找的执行可以被并发观察者观察为不同的操作,从而破坏了语句的“原子”行为。除非采取特殊的预防措施,否则多个线程可以尝试插入相同的值,都确信它们已经检查过并且该值不存在。只有一个能成功,另一个会打PK违例。 QED。

【讨论】:

@Peter:仅仅因为在自动事务中并不能使其成为原子。 子查询扫描并抓取尽快释放的 S 锁。没有任何东西可以从另一个线程删除该线程刚刚读取的行。这就是为什么接受答案中的 UPDLOCK 很重要。 开枪,你是对的。我很高兴在这样的一个重要问题上得到了纠正,但是现在我有很多代码要修复,而且我还没有为条件插入制定出一组最佳的表提示/查询结构。 HOLOCK, TABLOCKX 似乎可以工作,但是很糟糕。谢谢你莱姆斯! TABLOCKX 太过分了,因为它太宽泛了,它限制了可扩展性。 HOLDLOCK 将无济于事,因为它仅适用于 S 锁,并且将防止 PK 违规。适当的提示是 UPDLOCK。对于高并发,ROWLOCK 提示也可能有所帮助。 我将再次尝试 UPDLOCK,但在带有相关子查询的条件插入的目标表上,它并不能防止 PK 违规。 TABLOCKX, HOLDLOCK 确实如此,或者只要脚本运行它就会如此。我希望找出一些提示组合,使条件插入能够像我想象的那样工作。在查看锁时,我不确定一个人是否可以逃脱少于一个 TABLOCK,因为您正在将新值与整个域进行比较。但是,我们会看到的。【参考方案2】:

在测试行是否存在时传递 updlock、rowlock、holdlock 提示。 Holdlock 确保所有插入都是序列化的; rowlock 允许对现有行进行并发更新。

如果您的 PK 是 bigint,更新可能仍会阻塞,因为内部散列对于 64 位值是退化的。

begin tran -- default read committed isolation level is fine

if not exists (select * from <table> with (updlock, rowlock, holdlock) where <PK = ...>
    -- insert
else
    -- update

commit

【讨论】:

【参考方案3】:

编辑:Remus 是正确的,带 where 子句的条件插入并不能保证相关子查询和表插入之间的状态一致。

也许正确的表格提示可以强制保持一致的状态。 INSERT &lt;table&gt; WITH (TABLOCKX, HOLDLOCK) 似乎有效,但我不知道这是否是条件插入的最佳锁定级别。

在 Remus 所描述的一项简单测试中,TABLOCKX, HOLDLOCK 显示插入量约为无表提示的 5 倍,并且没有 PK 错误或课程。

原始答案,不正确:

这是原子的吗?

是的,带 where 子句的条件插入是原子的,您的 INSERT ... WHERE NOT EXISTS() ... UPDATE 表单是执行 UPSERT 的正确方法。

我会在 INSERT 和 UPDATE 之间添加IF @@ROWCOUNT = 0

INSERT INTO <table>
SELECT <natural keys>, <other stuff...>
WHERE NOT EXISTS
   -- no race condition here
   ( SELECT 1 FROM <table> WHERE <natural keys> )

IF @@ROWCOUNT = 0 BEGIN
  UPDATE ...
  WHERE <natural keys>
END

单个语句总是在事务中执行,无论是它们自己的(autocommit 和 implicit)还是与其他语句(explicit)一起执行。

【讨论】:

感谢您提供这些资源!正如 Remus 指出的那样,这并不能保证它是原子的,所以我将不得不使用 Arthur 的显式锁定方法,即使它更丑 IMO :( @rabidpebble:据我所知,Remus 错了。语句总是在事务中执行,事务保证原子性。如果没有,我们为什么还要打扰多语句显式事务?【参考方案4】:

您可以使用应用程序锁:(sp_getapplock) http://msdn.microsoft.com/en-us/library/ms189823.aspx

【讨论】:

【参考方案5】:

我见过的一个技巧是尝试 INSERT,如果失败,则执行 UPDATE。

【讨论】:

条件原子插入通常比 TRY CATCH 块快。请参阅此处进行一些基准测试:***.com/questions/1688618/…

以上是关于SQL Server 2005 中的原子 UPSERT的主要内容,如果未能解决你的问题,请参考以下文章

SQL Server 2005 中的系统视图文本

SQL SERVER 2005 中的维护计划有啥用

SQL Server 2005 中的数据聚合

启动SQL 2005服务 中的 SQL Server(MSSQLSERVER)是报错:请求失败,或未及时响应。

sql server 2005中的自动数据库备份

更新 SQL Server 2005 中的表列