关于sql:如何在PostgreSQL中使用RETURNING和ON CONFLICT?

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 |
----------

如果没有冲突,我想返回新的id列,或者返回冲突列的现有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

source列是一个可选的添加,用于演示如何工作。实际上,您可能需要它来区分这两种情况(与空写入相比还有一个优势)。好的。

由于附加数据修改CTE中新插入的行在基础表中尚不可见,因此最终的JOIN chats可以工作。(同一SQL语句的所有部分都可以看到基础表的相同快照。)好的。

由于VALUES表达式是独立的(不直接附加到INSERT上),Postgres无法从目标列派生数据类型,您可能需要添加显式类型转换。手册:好的。

When VALUES is used in INSERT, 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和附加的SELECT的开销(这应该是便宜的,因为完美的索引在定义上是存在的——一个唯一的约束是用一个索引实现的),查询本身对于很少的重复可能要贵一些。好的。

对于许多副本来说可能更快。额外写入的有效成本取决于许多因素。好的。

但在任何情况下,副作用和隐藏成本都会减少。总的来说,它可能更便宜。好的。

(附加的序列仍然是高级的,因为在测试冲突之前会填充默认值。)好的。

关于CTEs:好的。

  • 选择类型查询是唯一可以嵌套的类型吗?
  • 消除关系除法中的重复select语句

同时写入负载

假设默认READ COMMITTED事务隔离。好的。

有关dba.se的回答及详细解释:好的。

  • 并发事务导致在插入时具有唯一约束的争用条件

抵御竞争条件的最佳策略取决于准确的需求、表和upserts中行的数量和大小、并发事务的数量、冲突的可能性、可用资源和其他因素…好的。并发问题1

如果一个并发事务已经写入了一行,而您的事务现在正试图向该行追加数据,那么您的事务必须等待另一行完成。好的。

如果另一个事务以ROLLBACK结束(或有任何错误,即自动ROLLBACK结束),则您的事务可以正常进行。次要副作用:序列号中的间隙。但没有丢失的行。好的。

如果另一个事务正常结束(隐式或显式的COMMIT),则您的INSERT将检测到冲突(UNIQUE索引/约束是绝对的)和DO NOTHING,因此也不会返回行。(由于行不可见,因此无法锁定下面的并发问题2中所示的行。)SELECT从查询开始时看到相同的快照,也无法返回尚不可见的行。好的。

结果集中缺少任何这样的行(即使它们存在于基础表中)!好的。

这可能是正常的。尤其是如果您没有像示例中那样返回行,并且知道行在那里,您会感到满意。如果这还不够好,有很多方法可以解决。好的。

您可以检查输出的行数,如果该语句与输入的行数不匹配,则可以重复该语句。可能对这种罕见的病例足够好。关键是启动一个新的查询(可以在同一个事务中),然后该查询将看到新提交的行。好的。

或者检查同一查询中丢失的结果行,并使用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 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
...

SELECT中添加一个锁子句,就像FOR UPDATE一样。好的。

这样,当释放所有锁时,竞争的写操作将一直等到事务结束。所以简短些。好的。

更多详细信息和解释:好的。

  • 如何在从插入返回时包含排除的行…论冲突
  • 在函数中选择或插入是否容易出现竞争条件?

Deadlocks?

通过按一致的顺序插入行来防止死锁。见:好的。

  • 尽管有冲突,但使用多行插入的死锁不执行任何操作

数据类型和类型转换作为数据类型模板的现有表…

独立的VALUES表达式中第一行数据的显式类型转换可能不方便。有很多方法可以解决这个问题。可以使用任何现有关系(表、视图等)作为行模板。目标表是用例的明显选择。自动将输入数据强制为适当的类型,如在INSERTVALUES子句中:好的。

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')
   )
   ...

对于某些数据类型(底部链接答案中的解释)来说,这不起作用。下一个技巧适用于所有数据类型:好的。…和名字

如果插入整行(表中的所有列-或至少一组前导列),也可以省略列名。假设示例中的表chats只使用了3列:好的。

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
   )
   ...

详细说明和更多备选方案:好的。

  • 更新多行时强制转换空类型

旁白:不要使用像"user"这样的保留字作为标识符。那是一把有子弹的脚枪。使用合法、小写、不带引号的标识符。我把它换成了usr。好的。好啊。


我有完全相同的问题,我用"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是INSERT查询的扩展,在约束冲突的情况下,可以定义为两种不同的行为:DO NOTHINGDO UPDATE

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)

注意,RETURNING也不返回任何内容,因为没有插入元组。现在,使用DO UPDATE可以对tuple执行操作,这与存在冲突。首先要注意的是,必须定义一个约束,该约束将用于定义存在冲突。

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)