0

鉴于:

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

4 回答 4

2

这个问题大约每周在 SO 上被问一次,答案几乎总是错误的。

这是正确的。

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如果插入的行计数 - 无论在您的 DBMS 中表示 - 返回 1。

上面的语法是绝对标准的,对锁定机制或隔离级别没有任何假设。如果它不起作用,则您的 DBMS 已损坏。

许多人错误地认为select执行“首先”并因此引入了竞争条件。不:那select. insert插入是原子的。没有种族。

于 2013-05-06T05:23:19.883 回答
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. 继续,update如果需要或select它的主键。
于 2013-09-16T03:05:48.160 回答
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

大多数代码取自他的答案,但固定为提供互斥语义。

于 2013-05-03T20:18:10.233 回答
0
IF EXISTS (SELECT 1 FROM foo WHERE [email] = 'thisemail')
BEGIN
    UPDATE foo...
END
ELSE
BEGIN
    INSERT INTO foo...
END
于 2013-05-03T18:17:01.583 回答