如何在PostgreSQL中将RETURNING和ON CONFLICT一起使用?


149

我在PostgreSQL 9.5中具有以下UPSERT:

INSERT INTO chats ("user", "contact", "name") 
           VALUES ($1, $2, $3), 
                  ($2, $1, NULL) 
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id;

如果没有冲突,则返回以下内容:

----------
    | id |
----------
  1 | 50 |
----------
  2 | 51 |
----------

但是,如果有冲突,则不会返回任何行:

----------
    | id |
----------

id如果没有冲突,我想返回新的列,或者返回id冲突列的现有列。
能做到吗?如果是这样,怎么办?


1
使用ON CONFLICT UPDATE该行,以便进行更改。然后RETURNING将其捕获。
Gordon Linoff '16

1
@GordonLinoff如果没有什么要更新怎么办?
Okku

1
如果没有要更新的内容,则表示没有冲突,因此它只插入新值并返回其ID
zola 2016年

1
您可以在这里找到其他方式。我很想知道两者在性能方面的区别。
Stanislasdrg恢复莫妮卡

Answers:


88

我遇到了完全相同的问题,即使我没有要更新的内容,我还是使用“执行更新”而不是“不执行任何操作”解决了该问题。在您的情况下,将是这样的:

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;

该查询将返回所有行,无论它们刚刚被插入还是之前已经存在。


11
这种方法的一个问题是,主键的序列号在每次冲突时都会递增(伪更新),这基本上意味着您可能会在序列中留下巨大的空白。任何想法如何避免这种情况?
Mischa

9
@Mischa:那又如何?序列永远不能保证一开始就没有缝隙,而且间隔也无所谓(如果确实如此,则序列是错误的事情)
a_horse_with_no_name

24
建议在大多数情况下使用此功能。我添加了答案为什么。
欧文·布兰德斯特

4
这个答案似乎并没有解决DO NOTHING原始问题的方面-对我来说,它似乎为所有行更新了非冲突字段(此处为“名称”)。
PeterJCLaw

如下面很长的答案所述,对于未更改的字段使用“执行更新”不是“干净的”解决方案,并且可能导致其他问题。
比尔·沃辛顿

202

目前接受的答案似乎确定为一个单一的目标冲突,冲突少,小元组和没有触发器。它可以避免暴力破解并发问题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
...

并向SELECTFOR 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


2
您暗示此方法不会在序列中产生间隙,但它们是:INSERT ...在冲突中,从我所看到的每次都不会增加序列
有害

1
不是那么重要,但是为什么序列号增加了呢?有没有办法避免这种情况?
2014年

1
@salient:就像我在上面添加的内容:测试冲突之前,会填充列默认值,并且永远不会回滚序列,以避免与并发写入冲突。
Erwin Brandstetter

7
难以置信。仔细看一下,就像魅力一样,易于理解。我仍然希望ON CONFLICT SELECT...有什么事情:)
Roshambo

3
难以置信。Postgres的创建者似乎在折磨用户。为什么不简单地使returning子句总是返回值,而不管是否有插入?
Anatoly Alekseev

16

Upsert是INSERT查询的扩展,可以在约束冲突的情况下使用两种不同的行为来定义:DO NOTHINGDO UPDATE

INSERT INTO upsert_table VALUES (2, 6, 'upserted')
   ON CONFLICT DO NOTHING RETURNING *;

 id | sub_id | status
----+--------+--------
 (0 rows)

还请注意RETURNING,由于没有插入任何元组,因此不返回任何内容。现在,有了DO UPDATE,就有可能在存在冲突的元组上执行操作。首先请注意,定义一个约束将很重要,该约束将用于定义存在冲突。

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)

2
总是获取受影响的行ID并知道它是插入还是向上插入的好方法。正是我所需要的。
Moby Duck

这仍在使用“执行更新”,其缺点已在讨论中。
比尔·沃辛顿

4

对于插入单个项目,我可能会在返回id时使用合并:

WITH new_chats AS (
    INSERT INTO chats ("user", "contact", "name")
    VALUES ($1, $2, $3)
    ON CONFLICT("user", "contact") DO NOTHING
    RETURNING id
) SELECT COALESCE(
    (SELECT id FROM new_chats),
    (SELECT id FROM chats WHERE user = $1 AND contact = $2)
);

2
WITH e AS(
    INSERT INTO chats ("user", "contact", "name") 
           VALUES ($1, $2, $3), 
                  ($2, $1, NULL) 
    ON CONFLICT("user", "contact") DO NOTHING
    RETURNING id
)
SELECT * FROM e
UNION
    SELECT id FROM chats WHERE user=$1, contact=$2;

使用的主要目的ON CONFLICT DO NOTHING是避免引发错误,但不会导致任何行返回。因此,我们需要另一个SELECT来获取现有的ID。

在此SQL中,如果由于冲突而失败,则将不返回任何内容,然后第二个SELECT将获取现有行;否则,返回第二行。如果成功插入,将有两个相同的记录,那么我们需要UNION合并结果。


该解决方案效果很好,并且避免了对数据库进行不必要的写入(更新)!真好!
西蒙C

0

我修改了Erwin Brandstetter的令人惊讶的答案,该答案不会增加顺序,也不会写锁定任何行。我不是PostgreSQL的新手,所以如果您发现此方法有任何缺点,请随时告诉我:

WITH input_rows(usr, contact, name) AS (
   VALUES
      (text 'foo1', text 'bar1', text 'bob1')  -- type casts in first row
    , ('foo2', 'bar2', 'bob2')
    -- more?
   )
, new_rows AS (
   SELECT 
     c.usr
     , c.contact
     , c.name
     , r.id IS NOT NULL as row_exists
   FROM input_rows AS r
   LEFT JOIN chats AS c ON r.usr=c.usr AND r.contact=c.contact
   )
INSERT INTO chats (usr, contact, name)
SELECT usr, contact, name
FROM new_rows
WHERE NOT row_exists
RETURNING id, usr, contact, name

假设表chats对列具有唯一约束(usr, contact)

更新:添加了来自头像的建议修订(如下)。谢谢!


1
而不是CASE WHEN r.id IS NULL THEN FALSE ELSE TRUE END AS row_exists只写r.id IS NOT NULL as row_exists。而不是WHERE row_exists=FALSE只写WHERE NOT row_exists
spatar
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.