假设:
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。
答案 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)。从我能够收集到的,这个问题的唯一可移植解决方案:
insert
新行。如果行已存在,则必须捕获将发生的错误。insert
)。update
或select
其主键。答案 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