目前接受的答案对于单个冲突目标、少量冲突、小元组和无触发器似乎还可以。它通过蛮力避免了并发问题1(见下文)。简单的解决方案具有吸引力,副作用可能不太重要。
然而,对于所有其他情况,请不要无需更新相同的行。即使表面上看不出任何差异,也会产生各种副作用:
它可能触发不应该触发的触发器。
它会对“无辜”的行进行写锁定,可能会给并发事务带来成本。
它可能使行看起来是新的,尽管它是旧的(事务时间戳)。
最重要的是,根据PostgreSQL的MVCC模型,无论行数据是否更改,UPDATE
都会为每个目标行写入一个新的行版本。这会导致UPSERT本身的性能损失,表膨胀,索引膨胀,对表上后续操作的性能损失,以及VACUUM
的成本。对于少量重复项,影响较小,但对于大多数重复项来说,影响非常大。
此外,有时候使用ON CONFLICT DO UPDATE
并不切实际,甚至不可能。请参考手册:
对于
ON CONFLICT DO UPDATE
,必须提供一个
conflict_target
。
如果涉及多个索引/约束,则不可能有一个单一的“冲突目标”。
针对多个互斥的部分索引的相关解决方案:
-
基于带有NULL值的唯一约束的UPSERT
或者处理多个唯一约束的方法:
-
在ON CONFLICT子句中使用多个conflict_target
回到主题,您可以在没有空更新和副作用的情况下实现(几乎)相同的效果。以下一些解决方案也适用于
ON CONFLICT DO NOTHING
(没有“冲突目标”),以捕获可能出现的
所有冲突,这可能是可取的,也可能不是。
没有并发写入负载
WITH input_rows(usr, contact, name) AS (
VALUES
(text 'foo1', text 'bar1', text 'bob1')
, ('foo2', 'bar2', 'bob2')
)
, ins AS (
INSERT INTO chats (usr, contact, name)
SELECT * FROM input_rows
ON CONFLICT (usr, contact) DO NOTHING
RETURNING id
)
SELECT 'i' AS source
, id
FROM ins
UNION ALL
SELECT 's' AS source
, c.id
FROM input_rows
JOIN chats c USING (usr, contact);
源代码列是一个可选的附加项,用于演示它的工作原理。您可能实际上需要它来区分这两种情况(与空写入相比的另一个优点)。
最终的JOIN chats之所以有效,是因为附加的数据修改CTE中新插入的行在底层表中尚不可见。(同一SQL语句的所有部分都看到底层表的相同快照。)
由于VALUES表达式是独立的(不直接附加到INSERT语句),Postgres无法从目标列推导出数据类型,因此您可能需要添加显式类型转换。手册:
当VALUES用于INSERT时,值会自动强制转换为相应目标列的数据类型。当它用于其他上下文时,可能需要指定正确的数据类型。如果条目都是带引号的文字常量,则仅强制转换第一个条目就足以确定所有条目的假定类型。
查询本身(不计算副作用)对于少量重复项可能会更昂贵,因为需要使用CTE和额外的SELECT(由于完美索引的存在,这应该是廉价的 - 唯一约束是通过索引实现的)。
对于许多重复项来说,可能会(更)快。额外写入的实际成本取决于许多因素。
但无论如何,副作用和隐藏成本都较少。整体上可能更便宜。
附加序列仍然是先进的,因为在测试冲突之前会填充默认值。
关于CTE:
- [SELECT类型查询是唯一可以嵌套的类型吗?](link1)
- [在关系除法中去重SELECT语句](link2)
使用并发写入负载时
假设默认的
READ COMMITTED
事务隔离级别。相关链接:
防止竞争条件的最佳策略取决于确切的要求、表中行数和UPSERTs的大小、并发事务的数量、冲突的可能性、可用资源和其他因素...
并发问题1
如果一个并发事务已经对一行进行了写操作,而你的事务现在尝试进行UPSERT操作,你的事务必须等待另一个事务完成。
如果另一个事务以ROLLBACK
(或任何错误,即自动ROLLBACK
)结束,你的事务可以正常进行。可能的副作用:顺序号中的间隔,但不会有丢失的行。
如果其他事务正常结束(隐式或显式的COMMIT),你的INSERT将检测到冲突(唯一索引/约束是绝对的),并且不执行任何操作(DO NOTHING),因此也不会返回该行。(如下所示的并发问题2中也无法锁定该行,因为它是不可见的。)SELECT从查询开始时看到的是相同的快照,也无法返回尚不可见的行。
任何这样的行都不会出现在结果集中(即使它们存在于底层表中)!
这可能是可以接受的。特别是如果你不返回行,就像示例中一样,并且满足于知道该行存在。如果这还不够好,有各种方法可以解决。
你可以检查输出的行数,并在它与输入的行数不匹配时重复执行语句。对于罕见的情况可能足够好。关键是启动一个新的查询(可以在同一个事务中),这样就可以看到新提交的行。
或者在同一个查询中检查缺失的结果行,并使用Alextoni的答案中展示的蛮力技巧进行覆盖。
WITH input_rows(usr, contact, name) AS ( ... )
, ins AS (
INSERT INTO chats AS c (usr, contact, name)
SELECT * FROM input_rows
ON CONFLICT (usr, contact) DO NOTHING
RETURNING id, usr, contact
)
, sel AS (
SELECT 'i'::"char" AS source
, id, usr, contact
FROM ins
UNION ALL
SELECT 's'::"char" AS source
, c.id, usr, contact
FROM input_rows
JOIN chats c USING (usr, contact)
)
, ups AS (
INSERT INTO chats AS c (usr, contact, name)
SELECT i.*
FROM input_rows i
LEFT JOIN sel s USING (usr, contact)
WHERE s.usr IS NULL
ON CONFLICT (usr, contact) DO UPDATE
SET name = c.name
RETURNING 'u'::"char" AS source
, id
)
SELECT source, id FROM sel
UNION ALL
TABLE ups;
就像上面的查询一样,但我们在返回完整结果集之前,添加了一个使用CTE ups
的步骤。大部分情况下,最后一个CTE什么都不做。只有当返回结果中缺少行时,我们才会使用蛮力。
这会增加一些开销。与现有行发生冲突越多,这种方法的性能优于简单的方法的可能性就越大。
一个副作用是,第二个UPSERT会以无序的方式写入行,因此如果三个或更多事务同时写入相同的行,就会重新引入死锁的可能性(见下文)。如果这是一个问题,你需要一个不同的解决方案 - 就像上面提到的重复整个语句。
并发问题2
如果并发事务可以写入受影响行的相关列,并且您必须确保在同一事务的后期阶段找到的行仍然存在,您可以在CTE ins
中以低成本的方式锁定现有行(否则将不会锁定):
...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE
...
并且还要在
SELECT
中添加一个
锁定子句,比如FOR UPDATE
。
这样可以使竞争的写操作等待事务结束,当所有锁都释放时。所以要简洁明了。
更多细节和解释:
死锁?
通过以
一致的顺序插入行来防止
死锁。请参见:
数据类型和类型转换
现有表作为数据类型模板...
对于独立的VALUES表达式中的第一行数据,显式类型转换可能会不方便。有一些解决方法。您可以使用任何现有的关系(表、视图等)作为行模板。目标表是显而易见的选择。输入数据会自动强制转换为适当的类型,就像在INSERT语句的VALUES子句中一样。
WITH input_rows AS (
(SELECT usr, contact, name FROM chats LIMIT 0)
UNION ALL
VALUES
('foo1', 'bar1', 'bob1')
, ('foo2', 'bar2', 'bob2')
)
...
这对于某些数据类型不起作用。请参见:
...和名称
这对于所有数据类型都适用。
在插入表的所有(前导)列时,您可以省略列名。假设示例中的表chats
只包含在UPSERT中使用的3列:
WITH input_rows AS (
SELECT * FROM (
VALUES
((NULL::chats).*)
('foo1', 'bar1', 'bob1')
, ('foo2', 'bar2', 'bob2')
) sub
OFFSET 1
)
...
除此之外:不要使用像“user”这样的
保留字作为标识符。那是一个陷阱。请使用合法的小写、不带引号的标识符。我已将其替换为“usr”。
ON CONFLICT UPDATE
使行发生更改,然后使用RETURNING
捕获它。 - Gordon Linoff