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

zol*_*ola 127 sql postgresql upsert sql-returning

我在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;
Run Code Online (Sandbox Code Playgroud)

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

----------
    | id |
----------
  1 | 50 |
----------
  2 | 51 |
----------
Run Code Online (Sandbox Code Playgroud)

但如果存在冲突,则不会返回任何行:

----------
    | id |
----------
Run Code Online (Sandbox Code Playgroud)

id如果没有冲突,我想返回新列,或者返回id冲突列的现有列.
可以这样做吗?如果是这样,怎么样?

Erw*_*ter 165

目前接受的答案似乎确定了一些冲突,小元组和没有触发器.并且它通过暴力避免并发问题1(见下文).简单的解决方案有其吸引力,副作用可能不那么重要.

但是,对于所有其他情况,请勿在不需要的情况下更新相同的行.即使您在表面上看不到任何差异,也会产生各种副作用:

  • 它可能触发不应该触发的触发器.

  • 它写入锁定"无辜"行,可能会产生并发事务的成本.

  • 它可能会使行看起来很新,虽然它很旧(事务时间戳).

  • 最重要的是,使用PostgreSQL的MVCC模型,无论行数据是否相同,都会以任一方式编写新的行版本.这会导致UPSERT本身的性能损失,表膨胀,索引膨胀,表上所有后续操作的性能损失,VACUUM成本.对于一些重复轻微的影响,但大规模的大部分受骗者.

没有空的更新和副作用,您可以(几乎)实现相同的效果.

没有并发写入负载

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
Run Code Online (Sandbox Code Playgroud)

ON CONFLICT DO UPDATE列是一个可选的附加内容,用于演示其工作原理.实际上,您可能需要它来区分两种情况(另一种​​优于空写的优势).

最终的ON CONFLICT DO UPDATE工作原理是因为附加的数据修改CTE中新插入的行在基础表中尚不可见.(同一SQL语句的所有部分都会看到基础表的相同快照.)

由于conflict_target表达式是独立的(不直接附加到ON CONFLICT DO NOTHING),Postgres无法从目标列派生数据类型,因此您可能必须添加显式类型转换.手册:

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

由于CTE和附加的开销(由于完美索引在定义中存在 - 使用索引实现了唯一约束),查询本身对于少数欺骗可能有点贵VALUES.

许多重复可能(更快).额外写入的有效成本取决于许多因素.

但无论如何,副作用和隐藏成本都会减少.它总体上可能更便宜.

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

关于CTE:

具有并发写入负载

假设默认INSERT事务隔离.

关于dba.SE的相关答案,详细解释如下:

防范竞争条件的最佳策略取决于确切的要求,表格和UPSERT中行的数量和大小,并发交易的数量,冲突的可能性,可用资源和其他因素......

并发问题1

如果并发事务已写入您的事务现在尝试UPSERT的行,则您的事务必须等待另一个事务完成.

如果另一个交易以VALUES(或任何错误,即自动INSERT)结束,您的交易可以正常进行.副作用小:序号中的间隙.但没有丢失的行.

如果另一个事务正常结束(隐式或显式SELECT),您READ COMMITTED将检测到冲突(ROLLBACK索引/约束是绝对的)ROLLBACK,因此也不会返回该行.(也无法锁定行,如下面的并发问题2所示,因为它不可见.)COMMIT从查询的开头看到相同的快照,也无法返回尚不可见的行.

结果集中缺少任何此类行(即使它们存在于基础表中)!

可能是好的.特别是如果你没有像示例那样返回行,并且知道行存在就感到满意.如果这还不够好,可以采取各种方法.

您可以检查输出的行计数,如果它与输入的行计数不匹配,则重复该语句.对于罕见的情况可能足够好.关键是要启动一个新查询(可以在同一个事务中),然后查看新提交的行.

或者检查同一查询中是否缺少结果行,并用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;
Run Code Online (Sandbox Code Playgroud)

这就像上面的查询,但INSERT在返回完整的结果集之前,我们再添加一个CTE步骤.最后一次CTE大部分时间都不会做任何事情.只有当返回的结果中缺少行时,我们才会使用暴力.

还有更多的开销.与预先存在的行冲突越多,它就越有可能胜过简单的方法.

一个副作用:第二个UPSERT不按顺序写行,所以如果写入相同行的三个或更多事务重叠,它会重新引入死锁的可能性(见下文).如果这是一个问题,您需要一个不同的解决方案.

并发问题2

如果并发事务可以写入受影响行的相关列,并且您必须确保在同一事务的稍后阶段仍然存在您找到的行,则可以使用以下方法以低成本方式锁定行:

...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE  -- never executed, but still locks the row
...
Run Code Online (Sandbox Code Playgroud)

并添加一个锁定子句UNIQUE,如DO NOTHING.

这使得竞争写入操作等到事务结束时,所有锁定都被释放.所以要简短一点.

更多细节和解释:

死锁?

通过以一致的顺序插入行来防御死锁.看到:

数据类型和强制转换

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

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

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')
   )
   ...
Run Code Online (Sandbox Code Playgroud)

这对某些数据类型不起作用(在底部的链接答案中有解释).下一个技巧适用于所有数据类型:

......和名字

如果插入整行(表的所有列 - 或至少一组前导列),也可以省略列名.假设SELECT示例中的表只使用了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
   )
   ...
Run Code Online (Sandbox Code Playgroud)

详细解释和更多替代方案:


旁白:不要使用像FOR UPDATE标识符这样的保留字.那是一个装满脚的枪.使用合法的,小写的,不带引号的标识符.我换了它VALUES.

  • 难以置信.一旦你仔细观察它就像一个魅力,易于理解.我仍然希望`ON CONFLICT SELECT ...`尽管:) (5认同)
  • 您暗示此方法不会在序列中产生间隙,但它们是: INSERT ... ON CONFLICT DO NOTHING DO NOTHING 每次从我所看到的情况下都会增加序列 (3认同)
  • @Roshambo:是的,那会更优雅。(我在这里添加了显式类型转换的替代方法。) (3认同)
  • 不是那么重要,但为什么序列号会增加?有没有办法避免这种情况? (2认同)
  • @sudosensei:删除“i”AS 源、“和”'s”AS 源“,仅此而已。 (2认同)
  • @suricactus:你看到上面答案的最后一章了吗?这个问题确实有解决方案。 (2认同)
  • 难以置信。Postgres的创建者似乎在折磨用户。为什么不简单地使* returning *子句始终返回值,而不管是否有插入? (2认同)
  • 精彩的答案,只有我将使用的“那是一把装弹的枪”这个词才增强了答案。谢谢。 (2认同)

小智 73

我有完全相同的问题,我使用'do update'而不是'什么都不做'来解决它,即使我没有任何更新.在你的情况下,它将是这样的:

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;
Run Code Online (Sandbox Code Playgroud)

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

  • 在大多数情况下,我不会***建议使用它.我添加了答案为什么. (22认同)
  • 这种方法的一个问题是,主键的序列号在每次冲突(伪造更新)时都会递增,这基本上意味着您可能会在序列中出现巨大的空白.任何想法如何避免? (10认同)
  • @Mischa:那又怎样?序列永远不会保证是无间隙的,并且间隙无关紧要(如果它们确实如此,序列是错误的事情) (8认同)
  • 这个答案似乎没有达到原始问题的"DO NOTHING"方面 - 对我来说,它似乎更新了所有行的非冲突字段(此处为"name"). (4认同)
  • 正如下面很长的答案中所讨论的,对未更改的字段使用“执行更新”并不是一个“干净”的解决方案,并且可能会导致其他问题。 (2认同)

Jau*_*era 15

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

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

 id | sub_id | status
----+--------+--------
 (0 rows)
Run Code Online (Sandbox Code Playgroud)

请注意,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)
Run Code Online (Sandbox Code Playgroud)

  • 这里仍然使用“Do Update”,其缺点已经讨论过。 (5认同)
  • 总是获得受影响的行ID的好方法,并知道它是插入还是插入.正是我需要的. (2认同)

Yu *_*ang 14

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;
Run Code Online (Sandbox Code Playgroud)

使用的主要目的ON CONFLICT DO NOTHING是避免抛出错误,但会导致无行返回。所以我们需要另一个SELECT来获取现有的 id。

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

  • 该解决方案效果很好,避免了对数据库进行不必要的写入(更新)!好的! (2认同)

Joã*_*aas 13

对于单个项目的插入,我可能会在返回 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)
);
Run Code Online (Sandbox Code Playgroud)

对于插入多个项目,您可以将值放在一个临时的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
Run Code Online (Sandbox Code Playgroud)

  • 重要的是要将 *Coalesce* 重命名为 *id*。`...选择合并 (...) AS id` (4认同)
  • 我最喜欢的解决方案 (2认同)

Ari*_*zis 11

如果您只想更新插入一行

\n

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

\n
WITH\n  extant AS (\n    SELECT id FROM chats WHERE ("user", "contact") = ($1, $2)\n  ),\n  inserted AS (\n    INSERT INTO chats ("user", "contact", "name")\n    SELECT $1, $2, $3\n    WHERE NOT EXISTS (SELECT FROM extant)\n    RETURNING id\n  )\nSELECT id FROM inserted\nUNION ALL\nSELECT id FROM extant\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

如果写锁是您用例中的一项功能,则可以SELECT FOR UPDATEextant表达式中使用。

\n

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

\n
SELECT id, TRUE AS inserted FROM inserted\nUNION ALL\nSELECT id, FALSE FROM extant\n
Run Code Online (Sandbox Code Playgroud)\n

  • 提示已经明确指出出了什么问题。固定的; 不知道为什么我把这些括号放在那里。 (2认同)

rea*_*520 5

以上面 Erwin 的回答为基础(顺便说一句,很棒的回答,如果没有它,永远不会到达这里!),这就是我结束的地方。它解决了一些额外的潜在问题 - 它通过对输入集执行 a允许重复(否则会引发错误)select distinct,并确保返回的 ID 与输入集完全匹配,包括相同的顺序并允许重复。

此外,还有一个对我来说很重要的部分,它使用CTE显着减少了不必要的序列改进的数量,new_rows只尝试插入那些不在那里的序列。考虑到并发写入的可能性,它仍然会在减少的集合中遇到一些冲突,但后面的步骤会解决这个问题。在大多数情况下,序列间隙并不是什么大问题,但是当您进行数十亿次 upsert 且冲突比例很高时,这可能会导致对 ID使用 anint或 a的区别bigint

尽管又大又丑,它的表现却非常出色。我用数百万个 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;
Run Code Online (Sandbox Code Playgroud)


归档时间:

查看次数:

44914 次

最近记录:

5 年,10 月 前