如何使用 INSERT ... ON CONFLICT ... 更新所有列?

How to update all columns with INSERT ... ON CONFLICT ...?

我有一个只有一个主键的 table。当我尝试执行插入操作时,可能会因尝试使用现有键插入一行而导致冲突。我想让插入更新所有列?这有什么简单的语法吗?我试图让它 "upsert" 所有列。

我正在使用 PostgreSQL 9.5.5。

UPDATE syntax要求明确命名目标列。 避免这种情况的可能原因:

  • 您有很多列,只想缩短语法。
  • 不知道 列名称,唯一列除外。

"All columns" 必须表示 “目标的所有列 table”(或至少 " table") 的前导列以匹配顺序和匹配数据类型。否则您无论如何都必须提供目标列名列表。

测试table:

CREATE TABLE tbl (
  id    int PRIMARY KEY
, text  text
, extra text
);

INSERT INTO tbl VALUES
  (1, 'foo')
, (2, 'bar')
;

1。 DELETE & INSERT 在单个查询中

不知道除id以外的任何列名。

仅适用于 “目标的所有列 table”。虽然该语法甚至适用于前导子集,但目标 table 中的多余列将重置为其各自的列默认值(默认为 NULL),其中 DELETEINSERT.

需要

UPSERT (INSERT ... ON CONFLICT ...) 来避免并发写入负载下的并发/锁定问题,这只是因为没有通用的方法来锁定 Postgres 中尚不存在的行 (value locking ).

您的特殊要求只影响UPDATE部分。可能的并发症不适用于 现有 行受到影响的情况。那些被正确锁定。进一步简化,您可以将案例减少到 DELETEINSERT:

WITH data(id) AS (              -- Only 1st column gets explicit name
   VALUES
      (1, 'foo_upd', 'a')       -- changed
    , (2, 'bar', 'b')           -- unchanged
    , (3, 'baz', 'c')           -- new
   )
, del AS (
   DELETE FROM tbl AS t
   USING  data d
   WHERE  t.id = d.id
   -- AND    t <> d              -- optional, to avoid empty updates
   )                             -- only works for complete rows
INSERT INTO tbl AS t
TABLE  data                      -- short for: SELECT * FROM data
ON     CONFLICT (id) DO NOTHING
RETURNING t.id;

在 Postgres MVCC 模型中,UPDATEDELETEINSERT 大致相同 - 除了一些具有并发、触发器、HOT 更新和大列的极端情况存储的值越界,“TOASTed”值。由于您无论如何都想替换所有行,只需删除 INSERT 之前的冲突行即可。删除的行保持锁定状态,直到提交事务。如果并发事务碰巧同时插入它们(在 [=16= 之后,但在 INSERT 之前),INSERT 可能只会找到以前不存在的键值的冲突行。

在这种特殊情况下,您会丢失受影响行的额外列值。没有例外。但是,如果竞争查询具有相同的优先级,那几乎不是问题:另一个查询赢得了 some 行。此外,如果另一个查询是一个类似的 UPSERT,它的备选方案是等待此事务提交,然后立即更新。 “胜利”可能是代价不菲的胜利。

关于“空更新”:

  • How do I (or can I) SELECT DISTINCT on multiple columns?

不,我的查询必须赢!

好的,你自找的:

WITH data(id) AS (                   -- Only 1st column gets explicit name
   VALUES                            -- rest gets default names "column2", etc.
     (1, 'foo_upd', NULL)            -- changed
   , (2, 'bar', NULL)                -- unchanged
   , (3, 'baz', NULL)                -- new
   , (4, 'baz', NULL)                -- new
   )
, ups AS (
   INSERT INTO tbl AS t
   TABLE  data                       -- short for: SELECT * FROM data
   ON     CONFLICT (id) DO UPDATE
   SET    id = t.id
   WHERE  false                      -- never executed, but locks the row!
   RETURNING t.id
   )
, del AS (
   DELETE FROM tbl AS t
   USING  data     d
   LEFT   JOIN ups u USING (id)
   WHERE  u.id IS NULL               -- not inserted!
   AND    t.id = d.id
   -- AND    t <> d                  -- avoid empty updates - only for full rows
   RETURNING t.id
   )
, ins AS (
   INSERT INTO tbl AS t
   SELECT *
   FROM   data
   JOIN   del USING (id)             -- conflict impossible!
   RETURNING id
   )
SELECT ARRAY(TABLE ups) AS inserted  -- with UPSERT
     , ARRAY(TABLE ins) AS updated;  -- with DELETE & INSERT

如何?

  • 第一个 CTE data 只是提供数据。可以是 table。
  • 第二个 CTE ups:UPSERT。有冲突的行 id 没有改变,但也 locked.
  • 第三个 CTE del 删除了冲突的行。他们保持锁定状态。
  • 第 4 个 CTE ins 插入 整行 。只允许同一笔交易
  • 最后的 SELECT 是可选的,以显示发生了什么。

检查空更新测试(之前和之后):

SELECT ctid, * FROM tbl; -- did the ctid change?

(注释掉)检查行中的任何更改 AND t <> d 即使使用 NULL 值也能正常工作,因为我们正在比较两个类型化的行值 according to the manual:

two NULL field values are considered equal, and a NULL is considered larger than a non-NULL

但是所有列都必须支持 = / <> 运算符才能进行行比较。参见:

  • How to query a json column for empty objects?

2。动态 SQL

这也适用于前导列的子集,保留现有值。

诀窍是让 Postgres 使用系统目录中的列名动态构建查询字符串,然后执行它。

代码见相关答案:

  • Update multiple columns in a trigger function in plpgsql

  • Bulk update of all columns

  • SQL update fields of one table from fields of another one

id 列不是第一列时,Erwin Brandstetter 的回答似乎失败了。

以下使用 one of his other answers 中的片段来重现我的 'return ins/ups' 功能:

DO
$do$
BEGIN
EXECUTE (
SELECT
'DROP TABLE IF EXISTS res_tbl; CREATE TABLE res_tbl AS
 WITH ins AS (
       INSERT INTO dest
       TABLE  src                      -- short for: SELECT * FROM data
       ON     CONFLICT (id) DO UPDATE
       SET    id = dest.id
       WHERE  false                    -- never executed, but locks the row!
       RETURNING id
    ),
    repl AS (
        UPDATE dest
        SET   (' || string_agg(          quote_ident(column_name), ',') || ')
            = (' || string_agg('src.' || quote_ident(column_name), ',') || ')
        FROM   src
        WHERE  src.id = dest.id
        AND    src <> dest             -- avoids empty updates ¹
        RETURNING dest.id
    )
 SELECT ARRAY(TABLE ins)  AS inserted  -- with UPSERT
      , ARRAY(TABLE repl) AS updated   -- with DYNAMIC UPDATE
;'
FROM   information_schema.columns
WHERE  table_name   = 'src'     -- table name, case sensitive
AND    table_schema = 'public'  -- schema name, case sensitive
AND    column_name <> 'id'      -- all columns except id)
);
END
$do$;

¹ 仅适用于所有列都具有可比性的全行更新(例如 jsonb 而不是 json)。