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

Posted

技术标签:

【中文标题】如何在 PostgreSQL 中使用 RETURNING 和 ON CONFLICT?【英文标题】:How to use RETURNING with ON CONFLICT in PostgreSQL? 【发布时间】:2016-04-14 23:50:04 【问题描述】:

我在 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 列。可以这样做吗? 如果可以,如何?

【问题讨论】:

使用ON CONFLICT UPDATE,所以行会有变化。然后RETURNING 将捕获它。 @GordonLinoff 如果没有要更新的内容怎么办? 如果没有要更新的内容,则表示没有冲突,所以它只是插入新值并返回它们的 id 你会发现其他方式here。不过,我很想知道两者在性能方面的区别。 【参考方案1】:

如果你只想插入一行

然后您可以通过使用简单的EXISTS 检查来显着简化事情:

WITH
  extant AS (
    SELECT id FROM chats WHERE ("user", "contact") = ($1, $2)
  ),
  inserted AS (
    INSERT INTO chats ("user", "contact", "name")
    SELECT ($1, $2, $3)
    WHERE NOT EXISTS (SELECT NULL FROM extant)
    RETURNING id
  )
SELECT id FROM inserted
UNION ALL
SELECT id FROM extant

由于没有ON CONFLICT 子句,因此没有更新——只有插入,并且仅在必要时。所以没有不必要的更新,没有不必要的写锁,没有不必要的序列增量。也不需要演员表。

如果写锁是您的用例中的一项功能,您可以在extant 表达式中使用SELECT FOR UPDATE

如果需要知道是否插入了新行,可以在顶层UNION添加标志列:

SELECT id, TRUE AS inserted FROM inserted
UNION ALL
SELECT id, FALSE FROM extant

【讨论】:

【参考方案2】:

currently accepted answer 似乎适用于单个冲突目标、很少的冲突、小的元组和没有触发器。它通过蛮力避免并发问题 1(见下文)。简单的解决方案有其吸引力,副作用可能不太重要。

但是,对于所有其他情况,不要在不需要的情况下更新相同的行。即使你表面上看不出有什么不同,也有各种副作用

它可能会触发不应触发的触发器。

它写锁定“无辜”行,可能会产生并发事务的成本。

这可能会使该行看起来很新,尽管它是旧的(事务时间戳)。

最重要的是,对于PostgreSQL's MVCC model,每个UPDATE 都会写入一个新的行版本,无论行数据是否更改。这会导致 UPSERT 本身的性能损失、表膨胀、索引膨胀、表上后续操作的性能损失、VACUUM 成本。对少数重复项的影响较小,但对大多数重复项大量

另外,有时使用ON CONFLICT DO UPDATE 是不切实际甚至不可能的。 The manual:

对于ON CONFLICT DO UPDATE,必须提供一个conflict_target

如果涉及多个索引/约束,单个“冲突目标”是不可能的。但这里是多个部分索引的相关解决方案:

UPSERT based on UNIQUE constraint with NULL values

回到主题,您可以(几乎)实现相同的效果,而不会出现空洞的更新和副作用。以下一些解决方案也适用于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:

VALUESINSERT中使用时,值全部自动 强制转换为相应目标列的数据类型。什么时候 它在其他上下文中使用,可能需要指定 正确的数据类型。如果条目都是引用的文字常量, 强制第一个足以确定所有假设的类型。

由于 CTE 的开销和额外的 SELECT(因为根据定义,完美的索引就在那里——唯一的约束是用索引实现的)。

对于 许多 个重复项,可能会(很多)更快。额外写入的有效成本取决于许多因素。

但无论如何,副作用和隐藏成本会更少。总体而言,它很可能更便宜。

附加的序列仍然是高级的,因为默认值在之前进行冲突测试。

关于 CTE:

Are SELECT type queries the only type that can be nested? Deduplicate SELECT statements in relational division

具有并发写入负载

假设默认READ COMMITTED transaction isolation。相关:

Concurrent transactions result in race condition with unique constraint on insert

防御竞争条件的最佳策略取决于确切的要求、表和 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

这使得竞争的写操作等到事务结束,此时所有的锁都被释放。所以要简短。

更多细节和解释:

How to include excluded rows in RETURNING from INSERT ... ON CONFLICT Is SELECT or INSERT in a function prone to race conditions?

死锁?

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

Deadlock with multi-row INSERTs despite ON CONFLICT DO NOTHING

数据类型和强制转换

现有表作为数据类型的模板...

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

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

这不适用于某些数据类型。见:

Casting NULL type when updating multiple rows

...和名字

这也适用于所有数据类型。

在插入表的所有(前导)列时,您可以省略列名。假设示例中的表 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

【讨论】:

您暗示此方法不会在连续剧中产生间隙,但它们是:INSERT ... ON CONFLICT DO NOTHING 每次都会从我所看到的内容中增加连续剧 没那么重要,但是为什么连续剧会增加呢?有没有办法避免这种情况? 难以置信。一旦你仔细看它,它就像一个魅力并且很容易理解。我仍然希望ON CONFLICT SELECT... 有什么东西 :) @Roshambo:是的,那会更优雅。 (我在这里添加了显式类型转换的替代方案。) 难以置信。 Postgres 的创建者似乎在折磨用户。为什么不简单地让 returning 子句总是返回值,而不管是否有插入?【参考方案3】:

我遇到了完全相同的问题,我使用“做更新”而不是“什么都不做”来解决它,即使我没有什么要更新的。在你的情况下,它会是这样的:

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;

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

【讨论】:

这种方法的一个问题是,主键的序列号在每次冲突时都会增加(虚假更新),这基本上意味着您最终可能会在序列中出现巨大的间隙。任何想法如何避免这种情况? @Mischa:那又怎样?序列从一开始就不能保证是无间隙的,间隙无关紧要(如果确实如此,那么序列就是错误的做法) 我会建议在大多数情况下使用它。我添加了一个答案为什么。 这个答案似乎没有达到原始问题的DO NOTHING 方面——对我来说,它似乎更新了所有行的非冲突字段(此处为“名称”)。 正如下面很长的答案中所讨论的,对未更改的字段使用“执行更新”不是“干净”的解决方案,可能会导致其他问题。【参考方案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)
);

对于多个项目的插入,您可以将这些值放在一个临时的WITH 上并稍后引用它们:

WITH chats_values("user", "contact", "name") AS (
    VALUES ($1, $2, $3),
           ($4, $5, $6)
), new_chats AS (
    INSERT INTO chats ("user", "contact", "name")
    SELECT * FROM chat_values
    ON CONFLICT("user", "contact") DO NOTHING
    RETURNING id
) SELECT id
    FROM new_chats
   UNION
  SELECT chats.id
    FROM chats, chats_values
   WHERE chats.user = chats_values.user
     AND chats.contact = chats_values.contact

【讨论】:

重要的是将 Coalesce 重命名为 id... SELECT COALESCE ( ... ) AS id @Madacol 同意,如果您想要 100% 的“兼容”版本的“插入...返回...”,您应该添加它,但大多数情况下结果是通过SQL 客户端,它忽略列名。为简单起见保持原样。 不仅减少对数据库的影响(避免锁定和写入),而且这种 COALESCE 方法显着提高了性能并且仍然易于阅读。很好的解决方案! 我最喜欢的解决方案【参考方案5】:

以 Erwin 的上述回答为基础(顺便说一句,回答非常棒,没有它就永远不会到这里!),这就是我最终的结果。它解决了几个额外的潜在问题 - 它允许重复(否则会引发错误),方法是在输入集上执行 select distinct,并且它确保返回的 ID 完全匹配输入集,包括相同的顺序并允许重复。

另外,对我来说很重要的一个部分是,它显着减少了不必要的序列推进次数,使用 new_rows CTE 只尝试插入那些不存在的序列。考虑到并发写入的可能性,它仍然会在该缩减集中遇到一些冲突,但后面的步骤会解决这个问题。在大多数情况下,序列间隙不是什么大问题,但是当您进行数十亿次更新插入时,冲突的百分比很高,使用intbigint 作为ID 可能会有所不同。

尽管它又大又丑,但它的性能非常好。我用数百万个 upsert、高并发、大量冲突对它进行了广泛的测试。坚如磐石。

我已将它打包为一个函数,但如果这不是您想要的,应该很容易看出如何转换为纯 SQL。我还将示例数据更改为简单的数据。

CREATE TABLE foo
(
  bar varchar PRIMARY KEY,
  id  serial
);
CREATE TYPE ids_type AS (id integer);
CREATE TYPE bars_type AS (bar varchar);

CREATE OR REPLACE FUNCTION upsert_foobars(_vals bars_type[])
  RETURNS SETOF ids_type AS
$$
BEGIN
  RETURN QUERY
    WITH
      all_rows AS (
        SELECT bar, ordinality
        FROM UNNEST(_vals) WITH ORDINALITY
      ),
      dist_rows AS (
        SELECT DISTINCT bar
        FROM all_rows
      ),
      new_rows AS (
        SELECT d.bar
        FROM dist_rows d
             LEFT JOIN foo f USING (bar)
        WHERE f.bar IS NULL
      ),
      ins AS (
        INSERT INTO foo (bar)
          SELECT bar
          FROM new_rows
          ORDER BY bar
          ON CONFLICT DO NOTHING
          RETURNING bar, id
      ),
      sel AS (
        SELECT bar, id
        FROM ins
        UNION ALL
        SELECT f.bar, f.id
        FROM dist_rows
             JOIN foo f USING (bar)
      ),
      ups AS (
        INSERT INTO foo AS f (bar)
          SELECT d.bar
          FROM dist_rows d
               LEFT JOIN sel s USING (bar)
          WHERE s.bar IS NULL
          ORDER BY bar
          ON CONFLICT ON CONSTRAINT foo_pkey DO UPDATE
            SET bar = f.bar
          RETURNING bar, id
      ),
      fin AS (
        SELECT bar, id
        FROM sel
        UNION ALL
        TABLE ups
      )
    SELECT f.id
    FROM all_rows a
         JOIN fin f USING (bar)
    ORDER BY a.ordinality;
END
$$ LANGUAGE plpgsql;

【讨论】:

【参考方案6】:

最简单、最高效的解决方案是

BEGIN;

INSERT INTO chats ("user", contact, name) 
    VALUES ($1, $2, $3), ($2, $1, NULL) 
ON CONFLICT ("user", contact) DO UPDATE
  SET name = excluded.name
  WHERE false
RETURNING id;

SELECT id
FROM chats
WHERE (user, contact) IN (($1, $2), ($2, $1));

COMMIT;

DO UPDATE WHERE false 锁定但不更新行,这是一个特性,而不是错误,因为它确保另一个事务无法删除该行。

一些 cmets 想要区分更新的行和创建的行。

在这种情况下,只需将txid_current() = xmin AS created 添加到选择中即可。

【讨论】:

如果您只是在SELECT 中返回插入集ID,为什么还需要DO UPDATE..WHERE falseRETURNING 子句?在 PG 12 中,如果没有 UPDATERETURNING 子句仍然不返回任何内容(根据 WHERE false 子句) @BrDaHa,我解释说:“锁定但不更新行......它确保另一个事务无法删除该行” 是的,你说“DO UPDATE WHERE false 锁定但不更新行”,我明白了。我在问为什么有 RETURNING 子句,而它实际上并没有返回任何东西。是否还需要RETURNING 子句来防止删除? @BrDaHa,哦,是的,我已经很久没有看到这个了,但我认为返回是不必要的。【参考方案7】:

我修改了 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) 具有唯一约束。

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

另一个更新,根据Revinand 评论:

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 (
   INSERT INTO chats (usr, contact, name)
   SELECT 
     c.usr
     , c.contact
     , c.name
   FROM input_rows AS r
   LEFT JOIN chats AS c ON r.usr=c.usr AND r.contact=c.contact
   WHERE r.id IS NULL
   RETURNING id, usr, contact, name
   )
SELECT id, usr, contact, name, 'new' as row_type
FROM new_rows
UNION ALL
SELECT id, usr, contact, name, 'update' as row_type
FROM input_rows AS ir
INNER JOIN chats AS c ON ir.usr=c.usr AND ir.contact=c.contact

我还没有测试过上述内容,但是如果您发现新插入的行被多次返回,那么您可以将 UNION ALL 更改为 UNION,或者(更好),只需删除完全是第一个查询。

【讨论】:

而不是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 很好的解决方案,但它没有回答问题。您的解决方案仅返回插入的行 @Revinand 好点;在下面添加了完整的查询。【参考方案8】:
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来合并结果。

【讨论】:

此解决方案运行良好,可避免对数据库进行不必要的写入(更新)!!不错! 哇...谢谢,伙计。万分感谢。这就像一个魅力。我有一个依赖项,我需要将 ID 插入另一个 CTE。【参考方案9】:

作为INSERT 查询的扩展,Upsert 可以定义为在约束冲突的情况下使用两种不同的行为: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)

【讨论】:

始终获取受影响的行 id 并知道它是插入还是更新插入的好方法。正是我需要的。 这个还是用“做更新”,缺点已经讨论过了。

以上是关于如何在 PostgreSQL 中使用 RETURNING 和 ON CONFLICT?的主要内容,如果未能解决你的问题,请参考以下文章

PostgreSQL存储过程-return语句

如何在PostgreSQL中将空转换为null?

PostgreSQL - 使用函数在选择查询中命名列

查询返回语句 - PostgreSQL

如何在简单的 PostgreSQL 脚本中使用变量?

PostgreSQL介绍以及如何开发框架中使用PostgreSQL数据库