鉴于:
customer[id BIGINT AUTO_INCREMENT PRIMARY KEY, email VARCHAR(30), count INT]
我想执行原子如下:更新的客户,如果他已经存在; 否则,插入一个新的客户。
在理论上,这听起来像一个非常适合SQL合并 ,但我使用的是不支持数据库与AUTO_INCREMENT列MERGE 。
https://stackoverflow.com/a/1727788/14731似乎表明,如果你对一个不存在的行执行查询或更新语句时,数据库将锁定索引,从而防止并发插入。
这种行为是由SQL标准保证的? 是否有没有这样的行为,任何数据库?
UPDATE:对不起,我应该提到这前面:该解决方案必须使用READ_COMMITTED事务隔离,除非那是不可能在这种情况下,我会接受使用的序列化。
回答我的问题,因为似乎有很多围绕主题混乱。 看起来:
-- 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'
)
是开放的比赛条件(见只有插入行,如果它尚不存在 )。 从我已经能够收集,唯一的便携式解决这个问题:
- 挑选一键合并反对。 这可能是主键或其他唯一的密钥,但必须有一个唯一的约束。
- 尝试
insert
新行。 你必须赶上如果行已经存在,将出现错误。 - 困难的部分已经结束。 在这一点上,该行是保证存在,你的事实,你拿着它写锁(由于保护的比赛条件
insert
来自之前的步骤)。 - 来吧,
update
,如果需要或select
它的主键。
使用罗素福克斯的代码,但使用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
从他的回答拍摄的大多数代码,而是固定在设置互斥语义。
这个问题是问关于每周一次的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
,如果插入的行数-返回1 -你的DBMS不论如何表达。
上述语法是绝对标准,不作任何锁定机制或隔离级别的假设。 如果它不工作,你的DBMS被打破。
很多人都误以为下select
执行“第一”,从而引入了竞争条件。 无:即select
是的一部分 insert
。 插入是原子。 没有比赛。
IF EXISTS (SELECT 1 FROM foo WHERE [email] = 'thisemail')
BEGIN
UPDATE foo...
END
ELSE
BEGIN
INSERT INTO foo...
END