在目前接受的答案似乎确定为一个单一的目标冲突,冲突少,小元组和没有触发器。它可以避免暴力破解并发问题1(参见下文)。简单的解决方案具有吸引力,其副作用可能不太重要。
对于所有其他情况,不过,也并非无需更新相同的行。即使您在表面上看不到任何差异,也有各种副作用:
它可能会触发不应触发的触发器。
它会写锁“无辜”的行,这可能会导致并发事务的开销。
尽管行很旧(事务时间戳记),但这可能会使行看起来很新。
最重要的是,使用PostgreSQL的MVCC模型UPDATE
,无论行数据是否更改,都会为每个行写入一个新的行版本。这将导致UPSERT本身的性能下降,表膨胀,索引膨胀,表上后续操作的性能下降,VACUUM
成本。对复制品影响不大,但影响很大对大多数。
另外,有时不实际甚至无法使用ON CONFLICT DO UPDATE
。手册:
对于ON CONFLICT DO UPDATE
中,conflict_target
必须提供一个。
一个 如果涉及多个索引/约束,则不可能一个 “冲突目标”。
您可以(几乎)获得相同的效果,而不会出现空白更新和副作用。以下某些解决方案也可以使用ON CONFLICT DO NOTHING
(没有“冲突目标”),以捕获可能出现的所有可能的冲突-这可能是或不希望的。
没有并发写入负载
WITH input_rows(usr, contact, name) AS (
VALUES
(text 'foo1', text 'bar1', text 'bob1') -- type casts in first row
, ('foo2', 'bar2', 'bob2')
-- more?
)
, ins AS (
INSERT INTO chats (usr, contact, name)
SELECT * FROM input_rows
ON CONFLICT (usr, contact) DO NOTHING
RETURNING id --, usr, contact -- return more columns?
)
SELECT 'i' AS source -- 'i' for 'inserted'
, id --, usr, contact -- return more columns?
FROM ins
UNION ALL
SELECT 's' AS source -- 's' for 'selected'
, c.id --, usr, contact -- return more columns?
FROM input_rows
JOIN chats c USING (usr, contact); -- columns of unique index
该source
列是可选的,以演示其工作原理。您实际上可能需要用它来区分两种情况之间的区别(相对于空写入的另一个优势)。
最终的JOIN chats
工作是因为附加的修改数据的CTE中新插入的行在基础表中尚不可见。(同一SQL语句的所有部分都看到基础表的相同快照。)
由于该VALUES
表达式是独立的(不直接附加到INSERT
),因此Postgres无法从目标列派生数据类型,因此您可能必须添加显式的类型转换。手册:
在VALUES
中使用时INSERT
,所有值都会自动强制转换为相应目标列的数据类型。在其他上下文中使用它时,可能有必要指定正确的数据类型。如果所有条目都是用引号引起来的文字常量,则强制第一个足以确定所有假定的类型。
查询本身(不计算副作用)对于少数人可能会更昂贵由于CTE和其他操作的开销,SELECT
这是很便宜的,因为根据定义存在完美的索引-使用索引)。
对于许多人来说可能更快重复可能。额外写入的有效成本取决于许多因素。
但是副作用和隐性成本更低无论如何,。总的来说,它最便宜。
附加的序列仍然是高级的,因为默认值在测试冲突。
关于CTE:
并发写入负载
假设默认READ COMMITTED
事务隔离。有关:
防御竞争条件的最佳策略取决于确切的要求,表和UPSERT中的行数和大小,并发事务数,冲突的可能性,可用资源和其他因素...
并发问题1
如果并发事务已写入一行,而您的事务现在尝试将该行写入UPSERT,则您的事务必须等待另一个事务完成。
如果其他事务以ROLLBACK
(或任何错误,即自动ROLLBACK
)结束,则您的事务可以正常进行。较小的可能的副作用:序列号中的差距。但没有丢失的行。
如果其他事务正常结束(隐式或显式COMMIT
),您INSERT
将检测到冲突(UNIQUE
索引/约束是绝对的)DO NOTHING
,因此也不会返回该行。(由于该行不可见,因此也无法如下面的并发问题2所示锁定该行。)在查询开始时,该行会看到相同的快照,并且也无法返回该不可见的行。SELECT
结果集中会丢失任何此类行(即使它们存在于基础表中)!
这样可能还可以。特别是如果您不像示例中那样返回行,并且知道该行在那里就很满意。如果这还不够好,可以采取多种方法解决。
您可以检查输出的行数,如果它与输入的行数不匹配,则重复该语句。对于罕见的情况可能已经足够了。关键是开始一个新查询(可以在同一事务中),然后将看到新提交的行。
或检查同一查询中是否缺少结果行,并用Alextoni的答案中展示的蛮力技巧覆盖那些结果行。
WITH input_rows(usr, contact, name) AS ( ... ) -- see above
, ins AS (
INSERT INTO chats AS c (usr, contact, name)
SELECT * FROM input_rows
ON CONFLICT (usr, contact) DO NOTHING
RETURNING id, usr, contact -- we need unique columns for later join
)
, sel AS (
SELECT 'i'::"char" AS source -- 'i' for 'inserted'
, id, usr, contact
FROM ins
UNION ALL
SELECT 's'::"char" AS source -- 's' for 'selected'
, c.id, usr, contact
FROM input_rows
JOIN chats c USING (usr, contact)
)
, ups AS ( -- RARE corner case
INSERT INTO chats AS c (usr, contact, name) -- another UPSERT, not just UPDATE
SELECT i.*
FROM input_rows i
LEFT JOIN sel s USING (usr, contact) -- columns of unique index
WHERE s.usr IS NULL -- missing!
ON CONFLICT (usr, contact) DO UPDATE -- we've asked nicely the 1st time ...
SET name = c.name -- ... this time we overwrite with old value
-- SET name = EXCLUDED.name -- alternatively overwrite with *new* value
RETURNING 'u'::"char" AS source -- 'u' for updated
, id --, usr, contact -- return more columns?
)
SELECT source, id FROM sel
UNION ALL
TABLE ups;
就像上面的查询一样,但是ups
在返回完整的结果集之前,我们还要在CTE上再增加一步。最后的CTE在大多数情况下什么都不做。仅当返回结果中缺少行时,我们才使用蛮力。
还有更多的开销。与先前存在的行的冲突越多,其胜过简单方法的可能性就越大。
副作用:第二个UPSERT顺序写入行,因此,如果三个或更多写入相同行的事务重叠,则会重新引入死锁的可能性(见下文)。如果这是一个问题,则需要一个不同的解决方案-像上面提到的那样重复整个语句。
并发问题2
如果并发事务可以写入受影响的行的相关列,并且您必须确保找到的行在同一事务的稍后阶段仍然存在,则可以廉价地将现有行锁定在CTE中ins
(否则将被解锁)与:
...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE -- never executed, but still locks the row
...
并向SELECT
FOR UPDATE
加上锁定子句,例如。
这使得竞争的写操作一直等到事务结束时才释放所有锁。所以要简短。
更多详细信息和解释:
僵局?
通过以一致的顺序插入行来防止死锁。看到:
数据类型和类型转换
现有表作为数据类型的模板...
独立VALUES
表达式中第一行数据的显式类型转换可能很不方便。有很多解决方法。您可以使用任何现有关系(表,视图,...)作为行模板。目标表是用例的明显选择。输入数据被自动强制转换为适当的类型,如中VALUES
的条款INSERT
:
WITH input_rows AS (
(SELECT usr, contact, name FROM chats LIMIT 0) -- only copies column names and types
UNION ALL
VALUES
('foo1', 'bar1', 'bob1') -- no type casts here
, ('foo2', 'bar2', 'bob2')
)
...
这不适用于某些数据类型。看到:
...和名字
这也适用于所有数据类型。
在插入表的所有(开头)列时,您可以省略列名。chats
示例中的假设表仅由UPSERT中使用的3列组成:
WITH input_rows AS (
SELECT * FROM (
VALUES
((NULL::chats).*) -- copies whole row definition
('foo1', 'bar1', 'bob1') -- no type casts needed
, ('foo2', 'bar2', 'bob2')
) sub
OFFSET 1
)
...
另外:请勿使用保留字等"user"
作为标识符。那是一副满载的步枪。使用合法的,小写的,未加引号的标识符。我将其替换为usr
。
ON CONFLICT UPDATE
该行,以便进行更改。然后RETURNING
将其捕获。