web-dev-qa-db-ja.com

PostgreSQLでON CONFLICTで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;

競合がない場合は、次のようなものが返されます。

----------
    | id |
----------
  1 | 50 |
----------
  2 | 51 |
----------

ただし、競合がある場合、行は返されません。

----------
    | id |
----------

競合がない場合は新しいid列を返すか、競合する列の既存のid列を返します。
これを行うことはできますか?その場合、方法?

110
zola

私はまったく同じ問題を抱えており、更新するものが何もないにもかかわらず、「何もしない」の代わりに「更新する」を使用してそれを解決しました。あなたの場合、次のようなものになります。

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;

このクエリは、挿入されたばかりか以前に存在していたかに関係なく、すべての行を返します。

63
Alextoni

現在受け入れられている回答 は、single競合ターゲット、few競合、小さなタプル、トリガーなしの場合は問題ないようです。そして、ブルートフォースでconcurrency issue 1(以下を参照)を回避します。シンプルなソリューションには魅力があり、副作用はそれほど重要ではないかもしれません。

ただし、他のすべての場合は、notを実行して、同じ行を必要なく更新します。表面に違いが見えなくても、さまざまな副作用があります。

  • 起動すべきではないトリガーを起動する場合があります。

  • 「無害な」行を書き込みロックし、並行トランザクションのコストが発生する可能性があります。

  • 古い(トランザクションのタイムスタンプ)にもかかわらず、行が新しいように見える場合があります。

  • 最も重要なことPostgreSQLのMVCCモデル を使用すると、行データが同じであるかどうかに関係なく、新しい行バージョンがどちらの方法でも書き込まれます。これにより、UPSERT自体、テーブルの膨張、インデックスの膨張、テーブルに対する後続のすべての操作のパフォーマンスの低下、VACUUMのコストが発生します。重複が少ない場合は軽微な影響ですが、ほとんどの場合はmassiveです。

Plus、時にはON CONFLICT DO UPDATEを使用することが実用的ではないか、不可能ですらあります。 マニュアル:

ON CONFLICT DO UPDATEの場合、conflict_targetを指定する必要があります。

空の更新や副作用なしで(ほぼ)同じことを達成できます。また、以下のソリューションのいくつかはON CONFLICT DO NOTHING(「競合ターゲット」なし)で動作し、all発生する可能性のある競合をキャッチします。 (望ましくない場合があります。)

同時書き込み負荷なし

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

source列は、これがどのように機能するかを示すためのオプションの追加です。両方のケースの違いを伝えるために実際に必要になる場合があります(空の書き込みに対する別の利点)。

最後のJOIN chatsは、アタッチされた data-modifying CTE から新しく挿入された行が基礎となるテーブルにまだ表示されていないため機能します。 (同じSQLステートメントのすべての部分は、基礎となるテーブルの同じスナップショットを参照します。)

VALUES式は独立している(INSERTに直接添付されない)ため、Postgresはターゲット列からデータ型を導出できず、明示的な型キャストを追加する必要がある場合があります。 マニュアル:

VALUESINSERTが使用されている場合、値はすべて、対応する宛先列のデータ型に自動的に強制変換されます。他のコンテキストで使用する場合、正しいデータ型を指定する必要がある場合があります。エントリがすべて引用符で囲まれたリテラル定数である場合、最初のものを強制するだけで、すべての想定される型を決定できます。

クエリ自体は、CTEのオーバーヘッドと追加のSELECTにより、fewdupesに対して少し高価になる可能性があります(完全なインデックスが定義により存在するため、安価であるはずです) -一意の制約がインデックスで実装されます)。

manyの複製では、(はるかに)高速になる可能性があります。追加書き込みの効果的なコストは、多くの要因に依存します。

ただし、副作用と隠れたコストが少ないがあります。おそらく全体的に安いでしょう。

(デフォルト値はbeforeでテストされ、競合のテストが行​​われるため、付加されたシーケンスは依然として高度です。)

CTEについて:

同時書き込み負荷

デフォルトのREAD COMMITTEDトランザクション分離を想定しています。

詳細な説明を含むdba.SEの関連回答:

競合状態を防ぐための最適な戦略は、正確な要件、テーブルとUPSERTの行の数とサイズ、同時トランザクションの数、競合の可能性、利用可能なリソース、その他の要因に依存します...

並行性の問題1

トランザクションがUPSERTを試行する行に並行トランザクションが書き込まれた場合、トランザクションは他のトランザクションが完了するまで待機する必要があります。

他のトランザクションがROLLBACK(またはエラー、つまり自動ROLLBACK)で終了する場合、トランザクションは正常に続行できます。軽微な副作用:連番のギャップ。しかし、行が欠落していません。

他のトランザクションが正常に終了した場合(暗黙的または明示的なCOMMIT)、INSERTは競合(UNIQUEインデックス/制約が絶対的)とDO NOTHINGを検出するため、行も返されません。 (notvisibleであるため、以下のconcurrency issue 2に示すように行をロックすることもできません。)SELECTは同じスナップショットをクエリの開始であり、まだ表示されていない行を返すこともできません。

そのような行は結果セットから欠落しています(基礎となるテーブルに存在する場合でも)!

これ現状のままで大丈夫かもしれません。特に、例のように行を返さず、行が存在することを知って満足している場合。それで十分でない場合は、さまざまな方法があります。

出力の行数を確認し、入力の行数と一致しない場合はステートメントを繰り返すことができます。まれなケースには十分かもしれません。重要なのは、新しいクエリ(同じトランザクション内にある可能性がある)を開始することです。これにより、新しくコミットされた行が表示されます。

Or欠落している結果行をチェックするwithin同じクエリとoverwriteAlextoni's answer

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;

上記のクエリと似ていますが、complete結果セットを返す前に、CTE upsを使用してもう1つのステップを追加します。その最後のCTEはほとんど何もしません。返された結果から行が欠落した場合にのみ、ブルートフォースを使用します。

さらにオーバーヘッドが増えます。既存の行との競合が多いほど、単純なアプローチよりもパフォーマンスが向上する可能性が高くなります。

1つの副作用:2番目のUPSERTは行を順不同で書き込むため、three or moreが同じ行に書き込むトランザクションが重複している場合、デッドロックの可能性を再導入します(以下を参照)。それが問題であれば、別の解決策が必要です。

並行性の問題2

並行トランザクションが影響を受ける行の関連する列に書き込むことができ、見つかった行が同じトランザクションの後の段階でまだ存在していることを確認する必要がある場合、次の方法でlock rowsを安価に実行できます。

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

そして、 ロック節をFOR UPDATEのようなSELECTにも追加します

これにより、すべてのロックが解放されると、競合する書き込み操作がトランザクションの終わりまで待機するようになります。だから簡単に。

詳細と説明:

デッドロック?

一貫性のある順序に行を挿入することにより、デッドロックから守ります。見る:

データ型とキャスト

データ型のテンプレートとしての既存のテーブル...

独立VALUES式のデータの最初の行に対する明示的な型キャストは不便な場合があります。それを回避する方法があります。既存のリレーション(テーブル、ビューなど)を行テンプレートとして使用できます。ターゲットテーブルは、ユースケースの明らかな選択です。入力データは、VALUESINSERT句のように、適切なタイプに自動的に強制されます。

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 needed
    , ('foo2', 'bar2', 'bob2')
   )
   ...

これは、一部のデータ型では機能しません(下部のリンクされた回答の説明)。次のトリックはallデータ型に対して機能します:

...および名前

行全体(テーブルのすべての列-または少なくともleading列のセット)を挿入する場合、列名も省略できます。例のテーブルchatsには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に置き換えました。

154

INSERTクエリの拡張であるアップサートは、制約の競合が発生した場合の2つの異なる動作、DO NOTHINGまたはDO 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)
11
Jaumzera