在SQL Server 2005中执行原子“UPSERT”(存在更新,否则为INSERT)的正确模式是什么?
我在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 ...
其中&lt;条件&gt;将是对自然键的评估。上述方法似乎都不能很好地处理并发问题。如果我不能拥有两个具有相同自然键的行,那么上述所有风险似乎都会在竞争条件场景中插入具有相同自然键的行。
我一直在使用以下方法,但我很惊讶不要在人们的回复中看到它,所以我想知道它有什么问题:
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以避免幻像读取问题?对于这样一个普遍的问题,这肯定是过度的吗?
答案 0 :(得分:29)
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>
对于第二种竞争条件,人们可能会争辩说,并发线程无论如何都会删除密钥,所以它并不是真正的丢失更新。
最佳解决方案通常是尝试最可能的情况,如果失败则处理错误(当然是在事务中):
除了正确性之外,这种模式对于速度来说也是最佳的:尝试插入和处理异常比进行虚假锁定更有效。锁定意味着逻辑页面读取(可能意味着物理页面读取),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);
然而,仅在几秒钟内就发生了主要密钥违规行为:
Ms 2627,Level 14,State 1,Line 4
违反PRIMARY KEY约束'PK__Test__24927208'。无法在对象'dbo.Test'中插入重复键。
为什么?你是正确的,SQL查询计划将在DELETE ... FROM ... JOIN
,WITH cte AS (SELECT...FROM ) DELETE FROM cte
以及许多其他情况下执行“正确的事情”。但在这些情况下存在重大差异:“子查询”指的是更新或删除操作的目标。对于这种情况,查询计划确实会使用适当的锁,事实上我在某些情况下这种行为很重要,比如在实现队列时Using tables as Queues。
但是在原始问题中,以及在我的示例中,查询优化器将子查询看作查询中的子查询,而不是某些需要特殊锁保护的特殊“扫描更新”类型查询。结果是子查询查找的执行可以被一个concurent观察者视为一个独特的操作,从而打破了语句的“原子”行为。除非采取特殊预防措施,否则多个线程可以尝试插入相同的值,并确信它们已经检查过并且该值尚未存在。只有一个可以成功,另一个会击中PK违规。 QED。
答案 1 :(得分:6)
在测试行是否存在时,传递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
答案 2 :(得分:3)
编辑:Remus是正确的,条件插入w / where子句不保证相关子查询和表插入之间的一致状态。
也许正确的表格提示可能会强制一致的状态。 INSERT <table> WITH (TABLOCKX, HOLDLOCK)
似乎有效,但我不知道这是否是条件插入的最佳锁定级别。
在一个像Remus所描述的那样简单的测试中,TABLOCKX, HOLDLOCK
表示没有表提示的插入量约为5倍,没有PK错误或过程。
原始答案,不正确:
这是原子的吗?
是的,条件插入w / 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)一起执行。
答案 3 :(得分:2)
我见过的一个技巧是尝试INSERT,如果失败,则执行UPDATE。
答案 4 :(得分:2)
您可以使用应用程序锁:(sp_getapplock) http://msdn.microsoft.com/en-us/library/ms189823.aspx