SQL Server 2005中的Atomic UPSERT

在SQL Server 2005中,执行一个primefaces“UPSERT”(UPDATE where exists,INSERT otherwise)的正确模式是什么?

我看到很多代码(例如,请参阅检查一行是否存在,否则插入 )与以下两部分模式:

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结合在一起。

这是primefaces吗? 我无法find这将在SQL Server文档中logging的位置。

编辑:我意识到这可以做交易,但我想我需要设置交易水平为SERIALIZABLE,以避免幻像读取问题? 这对于这样一个普遍的问题肯定是过度的?

 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运行时已经消失。

对于第二种竞争条件,人们可能会认为密钥本来会被并发线程删除,所以它并不是真正的丢失更新。

最佳的解决scheme通常是尝试最有可能的情况,并处理错误(当然是在一个事务中):

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

除了正确性之外,这种模式对于速度也是最优的:尝试插入和处理exception比伪造locking更有效。 locking意味着逻辑页面读取(这可能意味着物理页面读取),IO(甚至逻辑)比SEH更昂贵。

更新 @Peter

为什么不是一个单一的声明“primefaces”? 假设我们有一个小桌子:

 create table Test (id int primary key); 

现在,如果我从两个线程运行这个单一语句,在一个循环中,就像你说的那样,它将是“primefaces”的,没有竞态条件可以存在:

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

然而在短短几秒钟内,就会发生主键违规:

Msg 2627,Level 14,State 1,Line 4
违反PRIMARY KEY约束“PK__Test__24927208”。 不能在对象'dbo.Test'中插入重复的键。

这是为什么? 你是正确的,SQL查询计划将做DELETE ... FROM ... JOINWITH cte AS (SELECT...FROM ) DELETE FROM cte和许多其他情况下的'正确的事情'。 但在这些情况下有一个关键的区别:“子查询”是指更新删除操作的目标 。 对于这样的情况,查询计划确实会使用适当的锁,事实上,这种行为在某些情况下是非常重要的,比如实现队列时使用表作为队列 。

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

在testing行的存在时传递updlock,rowlock,holdlock提示。 Holdlock确保所有插入都被序列化; 行锁允许对现有行进行并发更新。

如果您的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 

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

也许正确的表格提示可能会强制一致的状态。 INSERT <table> WITH (TABLOCKX, HOLDLOCK)似乎工作,但我不知道如果这是一个条件插入的最佳locking水平。

在像Remus所描述的一个简单的testing中, TABLOCKX, HOLDLOCK显示了无表提示的插入量的5倍,没有PK错误或过程。

原始答案,错误:

这是primefaces吗?

是的,条件插入w / where子句是primefaces的,并且您的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 

单个语句总是在一个事务中执行,或者是自己的( 自动提交和隐含的 ),或者与其他语句( 显式的 )一起执行。

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

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