currently accepted answer 似乎适用于单个冲突目标、很少的冲突、小的元组和没有触发器。它通过蛮力避免并发问题 1(见下文)。简单的解决方案有其吸引力,副作用可能不太重要。
但是,对于所有其他情况,不要在不需要的情况下更新相同的行。即使你表面上看不出有什么不同,也有各种副作用:
另外,有时使用ON CONFLICT DO UPDATE 是不切实际甚至不可能的。 The manual:
对于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 有效,因为来自附加data-modifying CTE 的新插入行在基础表中尚不可见。 (同一 SQL 语句的所有部分都看到相同的基础表快照。)
由于VALUES 表达式是独立的(不直接附加到INSERT),Postgres 无法从目标列派生数据类型,您可能必须添加显式类型转换。 The manual:
VALUES在INSERT中使用时,值全部自动
强制转换为相应目标列的数据类型。什么时候
它在其他上下文中使用,可能需要指定
正确的数据类型。如果条目都是引用的文字常量,
强制第一个足以确定所有假设的类型。
由于 CTE 的开销和额外的 SELECT(因为根据定义,完美的索引就在那里——唯一的约束是用索引实现的)。
对于 许多 个重复项,可能会(很多)更快。额外写入的有效成本取决于许多因素。
但无论如何,副作用和隐藏成本会更少。总体而言,它很可能更便宜。
附加的序列仍然是高级的,因为默认值在之前进行冲突测试。
关于 CTE:
具有并发写入负载
假设默认READ COMMITTED transaction isolation。相关:
防御竞争条件的最佳策略取决于确切的要求、表和 UPSERT 中行的数量和大小、并发事务的数量、冲突的可能性、可用资源和其他因素...
并发问题1
如果并发事务已写入您的事务现在尝试 UPSERT 的行,则您的事务必须等待另一个事务完成。
如果另一笔交易以ROLLBACK结束(或任何错误,即自动ROLLBACK),您的交易可以正常进行。可能的次要副作用:序号中的空白。但没有丢失的行。
如果其他事务正常结束(隐式或显式COMMIT),您的INSERT 将检测到冲突(UNIQUE 索引/约束是绝对的)和DO NOTHING,因此也不会返回该行。 (也无法锁定该行,如下面的并发问题 2 所示,因为它不可见。)SELECT 从查询开始看到相同的快照,并且无法返回尚不可见的行。
结果集中缺少任何此类行(即使它们存在于基础表中)!
这可能没问题。特别是如果您没有像示例中那样返回行并且知道该行在那里感到满意。如果这还不够好,有多种方法可以绕过它。
您可以检查输出的行数,如果它与输入的行数不匹配,则重复该语句。对于罕见的情况可能已经足够了。关键是启动一个新查询(可以在同一个事务中),然后它将看到新提交的行。
或检查同一查询内中丢失的结果行,并覆盖使用Alextoni's answer中演示的蛮力技巧。 p>
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;
与上面的查询类似,但在返回完整结果集之前,我们使用 CTE ups 多加了一步。最后一个 CTE 大部分时间都不会做任何事情。只有当返回的结果中缺少行时,我们才会使用暴力破解。
还有更多开销。与预先存在的行的冲突越多,这就越有可能优于简单方法。
一个副作用:第二个 UPSERT 写入行乱序,因此如果写入相同行的三个或更多事务重叠,它会重新引入死锁的可能性(见下文)。如果这是一个问题,您需要一个不同的解决方案 - 比如重复上面提到的整个语句。
并发问题2
如果并发事务可以写入受影响行的相关列,并且您必须确保在同一事务的稍后阶段找到的行仍然存在,您可以锁定现有行便宜在 CTE ins(否则会解锁)中:
...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE -- never executed, but still locks the row
...
并添加locking clause to the SELECT as well, like FOR UPDATE。
这使得竞争的写操作等到事务结束,此时所有的锁都被释放。所以要简短。
更多细节和解释:
死锁?
通过以一致的顺序插入行来防止死锁。见:
数据类型和强制转换
现有表作为数据类型的模板...
独立VALUES 表达式中第一行数据的显式类型转换可能不方便。有办法解决它。您可以使用任何现有的关系(表、视图、...)作为行模板。目标表是用例的明显选择。输入数据被自动强制转换为适当的类型,例如在INSERT 的VALUES 子句中:
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" 一样使用reserved words 作为标识符。那是一把上膛的足枪。使用合法的、小写的、不带引号的标识符。我将其替换为usr。