How to use RETURNING with ON CONFLICT in PostgreSQL?
我在PostgreSQL 9.5中有以下更新:
1 2 3 4 5 | INSERT INTO chats ("user","contact","name") VALUES ($1, $2, $3), ($2, $1, NULL) ON CONFLICT("user","contact") DO NOTHING RETURNING id; |
如果没有冲突,则返回如下内容:
1 2 3 4 5 6 7 | ---------- | id | ---------- 1 | 50 | ---------- 2 | 51 | ---------- |
但如果存在冲突,它不会返回任何行:
1 2 3 | ---------- | id | ---------- |
如果没有冲突,我想返回新的
对于很少的冲突、小元组和没有触发器,当前接受的答案似乎是可以的。它避免了使用蛮力的并发问题1(见下文)。简单的解决方案有其吸引力,副作用可能不那么重要。好的。
但是,对于所有其他情况,不需要更新相同的行。即使在表面上看不到任何差异,也会有各种副作用:好的。
它可能会触发不应触发的触发器。好的。
它写锁定"无害"的行,可能会产生并发事务的成本。好的。
它可能会使行看起来像新的,尽管它是旧的(事务时间戳)。好的。
最重要的是,使用PostgreSQL的MVCC模型,无论行数据是否相同,都会以任何方式编写新的行版本。这会导致upsert本身的性能损失、表膨胀、索引膨胀、表上所有后续操作的性能损失、
VACUUM 成本。对少数复制品的轻微影响,但对多数复制品的影响很大。好的。
在没有空更新和副作用的情况下,您可以实现(几乎)相同的结果。好的。无并发写入负载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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 |
由于附加数据修改CTE中新插入的行在基础表中尚不可见,因此最终的
由于
When
VALUES is used inINSERT , the values are all automatically
coerced to the data type of the corresponding destination column. When
it's used in other contexts, it might be necessary to specify the
correct data type. If the entries are all quoted literal constants,
coercing the first is sufficient to determine the assumed type for all.Ok.
由于CTE和附加的
对于许多副本来说可能更快。额外写入的有效成本取决于许多因素。好的。
但在任何情况下,副作用和隐藏成本都会减少。总的来说,它可能更便宜。好的。
(附加的序列仍然是高级的,因为在测试冲突之前会填充默认值。)好的。
关于CTEs:好的。
- 选择类型查询是唯一可以嵌套的类型吗?
- 消除关系除法中的重复select语句
同时写入负载
假设默认
有关dba.se的回答及详细解释:好的。
- 并发事务导致在插入时具有唯一约束的争用条件
抵御竞争条件的最佳策略取决于准确的需求、表和upserts中行的数量和大小、并发事务的数量、冲突的可能性、可用资源和其他因素…好的。并发问题1
如果一个并发事务已经写入了一行,而您的事务现在正试图向该行追加数据,那么您的事务必须等待另一行完成。好的。
如果另一个事务以
如果另一个事务正常结束(隐式或显式的
结果集中缺少任何这样的行(即使它们存在于基础表中)!好的。
这可能是正常的。尤其是如果您没有像示例中那样返回行,并且知道行在那里,您会感到满意。如果这还不够好,有很多方法可以解决。好的。
您可以检查输出的行数,如果该语句与输入的行数不匹配,则可以重复该语句。可能对这种罕见的病例足够好。关键是启动一个新的查询(可以在同一个事务中),然后该查询将看到新提交的行。好的。
或者检查同一查询中丢失的结果行,并使用Alextoni答案中演示的暴力技巧覆盖这些结果行。好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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
还有更多的开销。与预先存在的行冲突越多,这就越有可能胜过简单的方法。好的。
一个副作用:第二个upsert将行写入顺序错误,因此如果三个或更多事务写入同一行重叠,它将重新引入死锁的可能性(见下文)。如果这是个问题,你需要一个不同的解决方案。好的。并发问题2
如果并发事务可以写入受影响行的相关列,并且必须确保在同一事务的稍后阶段找到的行仍然存在,则可以使用以下方法以较低的成本锁定行:好的。
1 2 3 4 | ... ON CONFLICT (usr, contact) DO UPDATE SET name = name WHERE FALSE -- never executed, but still locks the row ... |
在
这样,当释放所有锁时,竞争的写操作将一直等到事务结束。所以简短些。好的。
更多详细信息和解释:好的。
- 如何在从插入返回时包含排除的行…论冲突
- 在函数中选择或插入是否容易出现竞争条件?
Deadlocks?
通过按一致的顺序插入行来防止死锁。见:好的。
- 尽管有冲突,但使用多行插入的死锁不执行任何操作
数据类型和类型转换作为数据类型模板的现有表…
独立的
1 2 3 4 5 6 7 8 | 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 needed , ('foo2', 'bar2', 'bob2') ) ... |
对于某些数据类型(底部链接答案中的解释)来说,这不起作用。下一个技巧适用于所有数据类型:好的。…和名字
如果插入整行(表中的所有列-或至少一组前导列),也可以省略列名。假设示例中的表
1 2 3 4 5 6 7 8 9 10 | 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 ) ... |
详细说明和更多备选方案:好的。
- 更新多行时强制转换空类型
旁白:不要使用像
我有完全相同的问题,我用"do update"而不是"do nothing"解决了它,尽管我没有什么要更新的。在您的情况下,应该是这样的:
1 2 3 4 | INSERT INTO chats ("user","contact","name") VALUES ($1, $2, $3), ($2, $1, NULL) ON CONFLICT("user","contact") DO UPDATE SET name=EXCLUDED.name RETURNING id; |
此查询将返回所有行,无论它们是否刚刚插入或以前存在。
upsert是
1 2 3 4 5 6 | INSERT INTO upsert_table VALUES (2, 6, 'upserted') ON CONFLICT DO NOTHING RETURNING *; id | sub_id | STATUS ----+--------+-------- (0 ROWS) |
注意,
1 2 3 4 5 6 7 8 | INSERT INTO upsert_table VALUES (2, 2, 'inserted') ON CONFLICT ON CONSTRAINT upsert_table_sub_id_key DO UPDATE SET STATUS = 'upserted' RETURNING *; id | sub_id | STATUS ----+--------+---------- 2 | 2 | upserted (1 ROW) |