PostgreSQL で ON CONFLICT と RETURNING をどのように組み合わせて使用​​するか? 質問する

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競合する列がある場合は既存の列を返します。
これは可能ですか?可能であれば、どのようにすればよいですか?

ベストアンサー1

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

ただし、他のすべてのケースでは、必要がない限り同一の行を更新しないでください。表面上は違いが見られない場合でも、さまざまな副作用があります。

  • 起動すべきでないトリガーが起動される可能性があります。

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

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

  • 最も重要なのはPostgreSQLのMVCCモデル UPDATE行データが変更されたかどうかに関係なく、すべてのターゲット行に対して新しい行バージョンを書き込みます。これにより、UPSERT 自体のパフォーマンスの低下、テーブルの肥大化、インデックスの肥大化、テーブルに対する後続の操作のパフォーマンスの低下、VACUUMコストが発生します。重複が少ない場合は影響は小さいですが、重複がほとんどの場合は影響は大きくなります

さらに、時には実用的ではなかったり、使用できないこともありますON CONFLICT DO UPDATEマニュアル:

の場合ON CONFLICT DO UPDATE、 を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

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

最終的にJOIN chats機能するのは、添付のデータ変更CTE基になるテーブルにはまだ表示されません。(同じ SQL ステートメントのすべての部分は、基になるテーブルの同じスナップショットを参照します。)

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

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

クエリ自体 (副作用は除く) は、CTE のオーバーヘッドと追加(定義により完全なインデックスが存在するため、これは安価であるはずで、一意の制約はインデックスを使用して実装されます) により、重複SELECTが少数の場合、少し高価になる可能性があります。

重複が多い場合は (はるかに) 高速になる可能性があります。追加書き込みの実際のコストは、多くの要因によって異なります。

しかし、いずれにしても副作用や隠れたコストは少なくなります。全体的にはおそらく安くなります。

競合をテストする前にデフォルト値が入力されるため、添付されたシーケンスは依然として高度です。

CTE について:

同時書き込み負荷あり

デフォルトを想定READ COMMITTEDトランザクション分離。 関連している:

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

同時実行の問題 1

同時実行トランザクションが、現在 UPSERT しようとしている行に書き込んでいる場合、トランザクションは他のトランザクションが終了するまで待機する必要があります。

他のトランザクションがROLLBACK(または何らかのエラー、つまり自動ROLLBACK)で終了した場合、トランザクションは正常に続行されます。軽微な副作用が発生する可能性があります: 連番にギャップが生じます。ただし、行が欠落することはありません。

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

このような行は、結果セットに存在しません (基になるテーブルに存在している場合でも)。

これは、そのままでも問題ないかもしれません。特に、例のように行を返さず、行が存在することを知っていれば十分である場合はそうです。それでも十分でない場合は、さまざまな回避策があります。

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

または、同じクエリ内で欠落している結果行をチェックし、それをブルートフォーストリックで上書きします。アレクストニの答え

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;

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

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

副作用の 1 つ: 2 回目の UPSERT は行を順序どおりに書き込まないため、同じ行に書き込む3 つ以上のトランザクションが重複すると、デッドロックの可能性が再び生じます (以下を参照)。これが問題になる場合は、上記のようにステートメント全体を繰り返すなど、別の解決策が必要です。

同時実行の問題 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不便な場合があります。回避策があります。既存のリレーション (テーブル、ビューなど) を行テンプレートとして使用できます。ターゲット テーブルは、ユース ケースの明らかな選択肢です。入力データは、VALUESの句のように、自動的に適切な型に強制変換されますINSERT

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

おすすめ記事