rab*_*ble 48 sql-server sql-server-2005 atomic upsert
在SQL Server 2005中执行原子"UPSERT"(在哪里存在UPDATE,否则为INSERT)的正确模式是什么?
我在SO上看到了很多代码(例如,请参阅检查是否存在行,否则插入),其中包含以下两部分模式:
UPDATE ...
FROM ...
WHERE <condition>
-- race condition risk here
IF @@ROWCOUNT = 0
INSERT ...
Run Code Online (Sandbox Code Playgroud)
要么
IF (SELECT COUNT(*) FROM ... WHERE <condition>) = 0
-- race condition risk here
INSERT ...
ELSE
UPDATE ...
Run Code Online (Sandbox Code Playgroud)
其中<condition>将是对自然键的评估.上述方法似乎都不能很好地处理并发问题.如果我不能拥有两个具有相同自然键的行,则似乎所有上述风险都会在竞争条件场景中插入具有相同自然键的行.
我一直在使用以下方法,但我很惊讶不要在人们的回复中看到它,所以我想知道它有什么问题:
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>
Run Code Online (Sandbox Code Playgroud)
请注意,此处提到的竞争条件与早期代码中的竞争条件不同.在早期的代码中,问题是幻读(在UPDATE/IF之间或在另一个会话的SELECT/INSERT之间插入行).在上面的代码中,竞争条件与DELETE有关.在(WHERE NOT EXISTS)执行之后但在INSERT执行之前,是否有可能由另一个会话删除匹配的行?目前还不清楚WHERE NOT EXISTS在何处与UPDATE一起锁定任何东西.
这是原子的吗?我无法找到SQL Server文档中记录的位置.
编辑: 我意识到这可以通过事务完成,但我想我需要将事务级别设置为SERIALIZABLE以避免幻像读取问题?对于这样一个常见的问题,这肯定是有点过分的吗?
Rem*_*anu 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>
Run Code Online (Sandbox Code Playgroud)
对于第二种竞争条件,人们可能会争辩说,并发线程无论如何都会删除该密钥,因此它并不是真正的丢失更新.
最佳解决方案通常是尝试最可能的情况,如果失败则处理错误(当然,在事务中):
除了正确性之外,这种模式对于速度来说也是最佳的:尝试插入和处理异常比进行虚假锁定更有效.锁定意味着逻辑页面读取(可能意味着物理页面读取),IO(甚至逻辑)比SEH更昂贵.
更新 @Peter
为什么单个语句不是"原子"?假设我们有一个简单的表格:
create table Test (id int primary key);
Run Code Online (Sandbox Code Playgroud)
现在,如果我从一个循环中的两个线程运行这个单一语句,它将是"原子",正如你所说,可以存在无争用条件:
insert into Test (id)
select top (1) id
from Numbers n
where not exists (select id from Test where id = n.id);
Run Code Online (Sandbox Code Playgroud)
然而,仅在几秒钟内,就会发生主键违规:
Msg 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和其他许多.但在这些情况下存在一个重要区别:"子查询"指的是更新或删除操作的目标.对于这种情况,查询计划确实会使用适当的锁,事实上我在某些情况下这种行为很关键,比如在实现队列时使用表作为队列.
但是在原始问题中,以及在我的示例中,查询优化器将子查询视为查询中的子查询,而不是某些需要特殊锁保护的特殊"扫描更新"类型查询.结果是,子查询查找的执行可以被一个concurent观察者观察为一个独特的操作,从而打破了语句的"原子"行为.除非采取特殊预防措施,否则多个线程可以尝试插入相同的值,并确信它们已经检查过并且该值尚未存在.只有一个可以成功,另一个会击中PK违规.QED.
在测试行是否存在时,传递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
Run Code Online (Sandbox Code Playgroud)
我见过的一个技巧是尝试插入,如果失败,则执行更新。
| 归档时间: |
|
| 查看次数: |
14040 次 |
| 最近记录: |