数据库在查询或更新后是否始终锁定不存在的行?

时间:2013-05-03 17:57:22

标签: sql insert locking h2

假设:

customer[id BIGINT AUTO_INCREMENT PRIMARY KEY, email VARCHAR(30), count INT]

我想以原子方式执行以下操作:如果客户已经存在,请更新客户;否则,请插入新客户。

从理论上讲,这听起来非常适合SQL-MERGE,但我使用的数据库不支持MERGE with AUTO_INCREMENT columns

https://stackoverflow.com/a/1727788/14731似乎表明如果对不存在的行执行查询或更新语句,数据库将锁定索引,从而阻止并发插入。

这种行为是否由SQL标准保证?是否有任何数据库不这样做?

更新:对不起,我之前应该提到过:解决方案必须使用READ_COMMITTED事务隔离,除非这是不可能的,在这种情况下我会接受使用SERIALIZABLE。

4 个答案:

答案 0 :(得分:2)

回答我自己的问题,因为围绕这个话题似乎有很多混乱。看来:

-- BAD! DO NOT DO THIS! --
insert customer (email, count) 
select 'foo@example.com', 0
where not exists (
      select 1 from customer
      where email = 'foo@example.com'
)

对比赛条件开放(见Only inserting a row if it's not already there)。从我能够收集到的,这个问题的唯一可移植解决方案:

  1. 选择要合并的密钥。这可能是主键或其他唯一键,但必须具有唯一约束。
  2. 尝试insert新行。如果行已存在,则必须捕获将发生的错误。
  3. 困难的部分结束了。此时,该行保证存在,并且您可以通过对其进行写锁定来保护您免受竞争条件的影响(由于上一步骤中的insert)。
  4. 如果需要,请updateselect其主键。

答案 1 :(得分:1)

使用Russell Fox的代码,但使用SERIALIZABLE隔离。这将采用范围锁定,以便逻辑上锁定不存在的行(与周围键范围内的所有其他不存在的行一起)。

所以它看起来像这样:

BEGIN TRAN
IF EXISTS (SELECT 1 FROM foo WITH (UPDLOCK, HOLDLOCK) WHERE [email] = 'thisemail')
BEGIN
    UPDATE foo...
END
ELSE
BEGIN
    INSERT INTO foo...
END
COMMIT

大多数代码取自他的答案,但修复了提供的互斥语义。

答案 2 :(得分:1)

每周一次询问这个问题,答案几乎总是错误的。

这是正确的。

insert customer (email, count) 
select 'foo@example.com', 0
where not exists (
      select 1 from customer
      where email = 'foo@example.com'
)

update customer set count = count + 1
where email = 'foo@example.com'

如果您愿意,可以插入1的计数并跳过update,如果插入的rowcount - 无论在DBMS中表达 - 返回1.

上述语法绝对是标准的,并不假设锁定机制或隔离级别。如果它不起作用,您的DBMS就会崩溃。

许多人误以为select执行“先”,因此引入了竞争条件。否:select insert的一部分。插入是原子的。没有比赛。

答案 3 :(得分:0)

IF EXISTS (SELECT 1 FROM foo WHERE [email] = 'thisemail')
BEGIN
    UPDATE foo...
END
ELSE
BEGIN
    INSERT INTO foo...
END