如何在PostgreSQL中使用ON CONFLICT RETURNING?

323

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


5
使用ON CONFLICT UPDATE使行发生更改,然后使用RETURNING捕获它。 - Gordon Linoff
4
如果没有需要更新的内容怎么办? - Okku
2
如果没有要更新的内容,这意味着没有冲突,因此它只会插入新值并返回它们的ID。 - zola
1
你可以在这里找到其他方法:http://dba.stackexchange.com/questions/129522/how-to-get-the-id-of-the-conflicting-row-in-upsert?newreg=73012b692b4f484d8406e4f67dd98ea6。不过我很想知道它们在性能方面的区别。 - Standaa - Remember Monica
10个回答

400

目前接受的答案对于单个冲突目标、少量冲突、小元组和无触发器似乎还可以。它通过蛮力避免了并发问题1(见下文)。简单的解决方案具有吸引力,副作用可能不太重要。

然而,对于所有其他情况,请不要无需更新相同的行。即使表面上看不出任何差异,也会产生各种副作用

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

  • 它会对“无辜”的行进行写锁定,可能会给并发事务带来成本。

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

  • 最重要的是,根据PostgreSQL的MVCC模型,无论行数据是否更改,UPDATE都会为每个目标行写入一个新的行版本。这会导致UPSERT本身的性能损失,表膨胀,索引膨胀,对表上后续操作的性能损失,以及VACUUM的成本。对于少量重复项,影响较小,但对于大多数重复项来说,影响非常大。

此外,有时候使用ON CONFLICT DO UPDATE并不切实际,甚至不可能。请参考手册:

对于ON CONFLICT DO UPDATE,必须提供一个conflict_target
如果涉及多个索引/约束,则不可能有一个单一的“冲突目标”。 针对多个互斥的部分索引的相关解决方案:
- 基于带有NULL值的唯一约束的UPSERT 或者处理多个唯一约束的方法:
- 在ON CONFLICT子句中使用多个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

源代码列是一个可选的附加项,用于演示它的工作原理。您可能实际上需要它来区分这两种情况(与空写入相比的另一个优点)。

最终的JOIN chats之所以有效,是因为附加的数据修改CTE中新插入的行在底层表中尚不可见。(同一SQL语句的所有部分都看到底层表的相同快照。)

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

当VALUES用于INSERT时,值会自动强制转换为相应目标列的数据类型。当它用于其他上下文时,可能需要指定正确的数据类型。如果条目都是带引号的文字常量,则仅强制转换第一个条目就足以确定所有条目的假定类型。

查询本身(不计算副作用)对于少量重复项可能会更昂贵,因为需要使用CTE和额外的SELECT(由于完美索引的存在,这应该是廉价的 - 唯一约束是通过索引实现的)。
对于许多重复项来说,可能会(更)快。额外写入的实际成本取决于许多因素。
但无论如何,副作用和隐藏成本都较少。整体上可能更便宜。
附加序列仍然是先进的,因为在测试冲突之前会填充默认值。
关于CTE:
- [SELECT类型查询是唯一可以嵌套的类型吗?](link1) - [在关系除法中去重SELECT语句](link2)
使用并发写入负载时
假设默认的READ COMMITTED事务隔离级别。相关链接:

防止竞争条件的最佳策略取决于确切的要求、表中行数和UPSERTs的大小、并发事务的数量、冲突的可能性、可用资源和其他因素...

并发问题1

如果一个并发事务已经对一行进行了写操作,而你的事务现在尝试进行UPSERT操作,你的事务必须等待另一个事务完成。

如果另一个事务以ROLLBACK(或任何错误,即自动ROLLBACK)结束,你的事务可以正常进行。可能的副作用:顺序号中的间隔,但不会有丢失的行。

如果其他事务正常结束(隐式或显式的COMMIT),你的INSERT将检测到冲突(唯一索引/约束是绝对的),并且不执行任何操作(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;

就像上面的查询一样,但我们在返回完整结果集之前,添加了一个使用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
...

并且还要在SELECT中添加一个锁定子句,比如FOR UPDATE
这样可以使竞争的写操作等待事务结束,当所有锁都释放时。所以要简洁明了。
更多细节和解释:

死锁?

通过以一致的顺序插入行来防止死锁。请参见: 数据类型和类型转换
现有表作为数据类型模板...
对于独立的VALUES表达式中的第一行数据,显式类型转换可能会不方便。有一些解决方法。您可以使用任何现有的关系(表、视图等)作为行模板。目标表是显而易见的选择。输入数据会自动强制转换为适当的类型,就像在INSERT语句的VALUES子句中一样。
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”。

5
您的意思是这种方法不会在序列中创建间隙,但实际上会有:从我所看到的情况来看,使用“INSERT ... ON CONFLICT DO NOTHING”每次都会递增序列。 - harmic
3
不是非常重要,但为什么序列号会递增呢?有没有办法避免这种情况? - salient
32
太不可思议了!一旦你仔细看过它,就非常好用且易于理解。虽然我仍然希望 ON CONFLICT SELECT... 这样的东西存在 :) - Roshambo
3
@Roshambo:是的,那会更加优雅。(我在这里添加了显式类型转换的替代方案。) - Erwin Brandstetter
25
令人难以置信。Postgres的创建者似乎在折磨用户。为什么不直接让*返回(returning)*子句始终返回值,而不管是否有插入呢? - Anatoly Alekseev
显示剩余32条评论

147

我曾经遇到过完全相同的问题,我通过使用“do update”而不是“do nothing”来解决它,即使我没有任何需要更新的内容。在你的情况下,代码会像这样:

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;

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


26
这种方法的问题在于,每次冲突(虚假更新)都会增加主键序列号,这基本上意味着你可能会在序列中得到巨大的间隙。有什么办法可以避免这种情况发生吗? - Mischa
17
@Mischa: 那又怎样?序列本来就不能保证没有间隔,而且间隔并不重要(如果重要的话,用序列是不合适的)。 - user330315
42
在大多数情况下,我建议不要使用这个。我添加了一个原因的答案。 - Erwin Brandstetter
8
这个回答似乎没有达到原问题中“什么都不做”的目的——对我来说,它似乎更新了所有行的非冲突字段(在这里是“name”)。 - PeterJCLaw
2
正如下面非常长的答案所讨论的那样,对于未更改的字段使用“Do Update”并不是一个“干净”的解决方案,可能会引起其他问题。 - Bill Worthington

54
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合并结果。

6
这个解决方案很好,避免了对数据库进行不必要的写入(更新)!! 很不错! - Simon C
哇...谢谢,伙计。非常感谢。这个方法非常有效。我有一个依赖项,需要将ID插入到另一个CTE中。 - md-shah
它并没有涉及原始问题的技术细节 - 但它确实很好地完成了工作。简单易用,可适应性强 - 谢谢! - David
为什么我有时会从这里得不到任何行?它不应该总是返回一些东西吗? - ptman

25

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

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中,然后稍后引用它们:

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;

注意: 根据Erwin的评论,假如你的应用程序会尝试同时“upsert”(两个工作线程同时插入一个相同的<unique_field> = 1),且在表中不存在这样的数据,那么在运行“upsert”之前,你应该更改事务的隔离级别:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
在这种情况下,两个事务中的一个将被中止。如果您的应用程序经常出现这种情况,您可能希望只执行2个单独的查询,否则,处理错误并重新执行查询会更容易和更快速。

4
重命名Coalesceid非常重要。 ... SELECT COALESCE ( ... ) AS id - Madacol
@Madacol同意,如果您想要一个100%“兼容”的“INSERT ... RETURNING ...”版本,那么应该添加它,但大多数情况下,结果会通过SQL客户端进行处理,该客户端会忽略列名。为了简单起见,保持原样即可。 - João Haas
不仅减少了数据库的影响(避免锁定和写入),而且这种 COALESCE 方法显著提高了性能,仍然易于阅读。非常好的解决方案! - cjn
2
我的最爱解决方案 - Kevin Pauli
在并发写入负载下不安全。您需要启动一个新查询来查看已经同时提交的行。 - Erwin Brandstetter
好的,但既然这是一个如此边缘的情况,我宁愿将隔离模式设置为“SERIALIZABLE”,并像处理其他错误一样处理该错误。不过我会在我的回复中添加一条注释。 - João Haas

23

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
12
仍在使用“Do Update”方式,其缺点已经被讨论过了。 - Bill Worthington

9

如果你只想插入或更新一行数据

那么你可以通过使用简单的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 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

1
我遇到了以下错误:Caused by: org.postgresql.util.PSQLException: ERROR: INSERT操作的目标列数大于表达式数量 Hint: 插入源是一个行表达式,其列数与INSERT所需的相同。您是否意外多使用了括号? - ka3ak
2
提示已经准确说明了问题所在。已修复;不知道为什么我会加上那些括号。 - Aristotle Pagaltzis
这对并发写入是安全的吗?如果两个工作进程尝试插入相同的行,但第一个查询对两者都返回空值,它会失败吗? - Barry
@Barry,很抱歉我不能确定地回答你的问题,但我倾向于认为它不安全(因为我不知道为什么会安全)。然而,我可以肯定地说,如果确实有必要,那么你可以通过执行我提到的操作使其变得安全:在extant CTE中使用SELECT FOR UPDATE来提前获取写锁。这将导致同一查询的并发执行在尝试选择现有行之前被阻塞,并且只允许在早期执行完成对表的写入后才能继续进行。 - Aristotle Pagaltzis

6

在Erwin上面的回答基础上构建(非常好的答案,没有它我永远也做不到!),这就是我的最终结果。它解决了一些额外的潜在问题——通过对输入集合进行select distinct,它允许重复值(否则会出错),并确保返回的ID与输入集合完全匹配,包括相同的顺序和允许重复。

另外,对我来说很重要的一部分是,使用new_rows CTE大大减少了不必要的序列推进次数,仅尝试插入尚未存在的那些。考虑到并发写入的可能性,它仍然会在缩小的集合中遇到一些冲突,但后续步骤会处理这些冲突。在大多数情况下,序列间隙并不是一个大问题,但当您进行数十亿次upsert时,并且高百分比存在冲突时,它可以使使用ID的intbigint之间的差异。

尽管它很大且丑陋,但它的表现非常出色。我进行了广泛的测试,包括数百万个upserts、高并发和高碰撞次数。非常可靠。

我将它封装为一个函数,但如果这不是您想要的,那么将其转换为纯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;

3
最简单、最高效的解决方案是:
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锁定但不更新行,这是一种特性而非错误,因为它确保另一个事务不能删除该行。
一些评论想区分已更新和已创建的行。
在这种情况下,请将txid_current() = xmin AS created 添加到选择语句中即可。

如果你只是在SELECT中返回插入集合的ID,为什么还需要DO UPDATE..WHERE falseRETURNING子句呢?在PG 12中,如果没有UPDATE(根据WHERE false子句),RETURNING子句仍然不返回任何内容。 - BrDaHa
@BrDaHa,我解释了:“锁定但不更新行...它确保另一个事务无法删除该行”。 - Paul Draper
1
是的,你说过“DO UPDATE WHERE false会锁定但不会更新行”,我理解了这一点。我想问的是,既然RETURNING子句实际上并没有返回任何内容,那么它为什么要存在呢?RETURNING子句是否也需要用于防止删除操作? - BrDaHa
1
@BrDaHa,噢,是的,我很久没有看过这个了,但我认为返回是不必要的。 - Paul Draper
如果您有多个唯一约束条件,请参见 https://dba.stackexchange.com/a/212634/4719 - Gili

1

我修改了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,或者(更好的办法)完全删除第一个查询。我还没有测试上述方法。

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
1
好的解决方案,但它并没有回答问题。你的解决方案只返回插入的行。 - Revinand
@Revinand 很好的观点;在下面添加了完整的查询。 - ChoNuff
一个缺点是它非常复杂。我更愿意设置我的数据,在唯一列值或引用另一条记录的基础上进行查询。 - MrMesees
@MrMesees 这需要向数据库发出另一个查询,这可能会减慢速度。此外,查询唯一值可能非常昂贵,特别是如果表没有为您的需求进行良好的索引。话虽如此,我上面的解决方案也高度依赖于索引策略才能表现良好,但我认为受到的影响比运行查询生成唯一值要小。 - ChoNuff
我已经接受了将行更新作为一种替代方案。使用许多这些解决方法,我引用作为为什么我会接受后果的原因。我所需要的就是唯一约束和冲突更新,我的返回语句就能工作。这个版本少于10行代码而不是40行,但在这10行中,与40行的版本相比,很容易出错,而且小于10行的版本非必要部分非常少[如果有]。 - MrMesees

-2

将字段更新为其当前值。在这种情况下,冲突行的数据不会更改,并且它将返回冲突行的 id 而不是空集。

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接