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

时间:2016-11-18 23:03:06

标签: sql database postgresql insert upsert

我有一个包含单个主键的表。当我尝试插入时,尝试插入具有现有密钥的行可能会导致冲突。我想允许插入更新所有列?这有什么简单的语法吗?我试图让它“插入”所有列。

我正在使用PostgreSQL 9.5.5。

2 个答案:

答案 0 :(得分:9)

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

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

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

测试表:

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

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

1。 DELETE&而是在单个查询中使用INSERT

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

仅适用于“目标表的所有列”。虽然语法甚至适用于前导子集,但目标表中的多余列将使用DELETEINSERT重置为NULL。

需要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更新和大列值的一些极端情况)线)。由于您仍想要替换所有行,只需删除INSERT之前的冲突行。在提交事务之前,已删除的行将保持锁定状态。 INSERT可能只发现以前不存在的键值存在冲突的行,如果并发事务碰巧同时插入它们(在DELETE之后但在INSERT之前)。

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

关于“空更新”:

不,我的查询必须赢!

好的,你问过它:

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只提供数据。可能是一张桌子。
  • 第二次CTE ups:UPSERT。有冲突id的行不会更改,但已锁定
  • 第3次CTE del删除有冲突的行。他们仍然被锁定。
  • 第4次CTE ins插入整行。仅允许进行相同的交易
  • 最终的SELECT仅用于演示以显示发生的事情。

使用以下方法检查空更新测试(之前和之后):

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

2。动态SQL

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

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

查看代码的相关答案:

答案 1 :(得分:0)

由于我缺乏评论的名声:当id列不在第一列时,Erwin Brandstetter的答案似乎失败了。

以下内容来自他的另一本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 - only for full-row updates where all columns are comparable (e.g. jsonb not json)
        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$;