web-dev-qa-db-ja.com

INSERT ... ON CONFLICT ...ですべての列を更新する方法

単一の主キーを持つテーブルがあります。挿入を行おうとすると、既存のキーを使用して行を挿入しようとすると競合が発生する可能性があります。挿入ですべての列を更新できるようにしたいですか?これには簡単な構文はありますか?私はそれがすべてのカラムを「upsert」させようとしています。

PostgreSQL 9.5.5を使用しています。

14
beatsforthemind

UPDATE syntaxrequiresターゲット列に明示的に名前を付ける。それを回避する考えられる理由:

  • 多くの列があり、構文を短くしたいだけです。
  • know一意の列を除き、列名は使用できません。

"All columns"は、一致する順序と一致するデータ型で"ターゲットテーブルのすべての列"(または少なくとも"テーブルの先頭列")を意味する必要があります。そうでない場合は、とにかくターゲット列名のリストを提供する必要があります。

テストテーブル:

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

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

1.代わりに単一のクエリでDELETEINSERT

id。以外の列名を知ることなく

"ターゲットテーブルのすべての列"に対してのみ機能します。構文は先頭のサブセットでも機能しますが、ターゲットテーブルの余分な列は、DELETEおよびINSERTを使用してNULLにリセットされます。

UPSERT(INSERT ... ON CONFLICT ...)は、同時書き込み負荷での同時性/ロックの問題を回避するために必要です。これは、Postgresにまだ存在しない行をロックする一般的な方法がないためです( 値のロック =)。

特別な要件はUPDATE部分にのみ影響します。 existing行が影響を受ける場合、起こり得る複雑化は適用されません。それらは適切にロックされています。さらに単純化すると、ケースを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モデルでは、UPDATEDELETEおよびINSERTとほとんど同じです(ただし、同時実行性、HOT更新、および格納されている大きな列の値を伴ういくつかの例外的なケースを除く)行の)。とにかくすべての行を置き換えたいので、INSERTの前に競合する行を削除します。削除された行は、トランザクションがコミットされるまでロックされたままになります。 INSERTは、同時トランザクションがそれらを同時に挿入した場合(DELETEの後、INSERTの前)に、以前は存在しなかったキー値の競合する行のみを検出する可能性があります。

この特殊なケースでは、影響を受ける行の追加の列値が失われます。例外は発生しません。ただし、競合するクエリの優先度が同じ場合、それはほとんど問題になりません。他のクエリはsome行に対して勝ちます。また、他のクエリが同様のUPSERTである場合、その代替策は、このトランザクションがコミットするのを待って、すぐに更新することです。 「勝利」はピューリクスの勝利かもしれない。

「空の更新」について:

いいえ、クエリは勝つ必要があります!

OK、あなたはそれを求めました:

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はデータを提供するだけです。代わりにテーブルである可能性があります。
  • 2番目のCTE ups:UPSERT。 idが競合する行は変更されませんが、lockedも変更されません。
  • 3番目のCTE delは、競合する行を削除します。彼らはロックされたままです。
  • 4番目のCTE ins行全体を挿入します。同じトランザクションでのみ許可
  • 最後のSELECTは、デモが何が起こったかを示すためのものです。

次のコマンドを使用して、空の更新テスト(前後)をチェックするには

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

2.動的SQL

これは、既存の値を保持しながら、主要な列のサブセットでも機能します。

トリックは、Postgresにシステムカタログの列名を含むクエリ文字列を動的に作成させ、それを実行させることです。

コードの関連回答を参照してください:

14

コメントする評判が足りないので、id列が最初の列ではない場合、Erwin Brandstetterの回答は失敗するようです。

以下は、彼の他の answers からのスニペットを使用して、私の場合の「リターン/アップ」機能を再現します。

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$;
0
David Brakman