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 ... JOIN
、WITH cte AS (SELECT...FROM ) DELETE FROM cte
和许多其他情况下执行“正确的事情”。但在这些情况下有一个关键的区别:“子查询”指的是 update 或 delete 操作的 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 <table> 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的主要内容,如果未能解决你的问题,请参考以下文章