PostgreSQLでON CONFLICTを使用してRETURNINGを使用する方法


149

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
ON CONFLICT UPDATE行に変更があるように使用します。その後、それRETURNINGをキャプチャします。
ゴードンリノフ2016年

1
@GordonLinoff更新するものが何もない場合はどうなりますか?
Okku、2016年

1
更新するものが何もない場合、それは競合がなかったことを意味するため、新しい値を挿入してそれらのIDを返すだけです
zola

1
他の方法はここにあります。ただし、パフォーマンスの点でこの2つの違いを知りたいです。
Stanislasdrgがモニカを復活させる

回答:


88

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

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;

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


11
このアプローチの1つの問題は、主キーのシーケンス番号が競合(偽の更新)のたびに増加することです。これは、基本的にシーケンスに大きなギャップが生じる可能性があることを意味します。それを避ける方法はありますか?
Mischa

9
@Mischa:だから何?シーケンスは最初からギャップがないことが保証されておらず、ギャップが問題にならない(そして、ギャップがある場合、シーケンスは間違った処理です)
a_horse_with_no_name

24
ほとんどの場合、これを使用することはお勧めしません。理由を追加しました。
Erwin Brandstetter 2017

4
この回答はDO NOTHING、元の質問の側面を達成するようには見えません-私にとっては、すべての行の非競合フィールド(ここでは、「名前」)を更新するようです。
PeterJCLaw

以下の非常に長い回答で説明されているように、変更されていないフィールドに「Do Update」を使用することは「クリーン」なソリューションではなく、他の問題を引き起こす可能性があります。
ビルワージントン

202

現在受け入れ答えは、単一の紛争の対象、いくつかの競合、小さなタプルなしトリガのOKらしいです。力ずくで同時実行問題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式は、自立(直接に接続されていないINSERT)Postgresがターゲット列から派生データ型ではないことができますし、明示的な型キャストを追加する必要があります。マニュアル:

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

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

多くの複製で(はるかに)高速かもしれません。追加書き込みの効果的なコストは、多くの要因に依存します。

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

デフォルト値が入力されているため、アタッチされたシーケンスはまだ高度です 競合をテストする前にれるため。

CTEについて:

同時書き込み負荷あり

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

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

並行性の問題1

現在のトランザクションがUPSERTを試みる行に同時トランザクションが書き込んだ場合、トランザクションは他のトランザクションが終了するまで待機する必要があります。

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

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

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

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

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

または、同じクエリで欠落している結果行を確認し、Alextoniの回答で示されているブルートフォーストリックでそれらを上書きします

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;

上記のクエリに似ていますが、CTEを使用してステップを1つ追加してupsから、完全なコードを返します。結果セットます。その最後の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
...

そして、同様にロッキング句を追加しますSELECTFOR 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


2
あなたを意味するもので、この方法は、雑誌のギャップを作成しませんが、彼らは次のとおりです。INSERT ... ON CONFLICT DO NOTHINGは私が見ることができるものからシリアルたびにインクリメントん
harmic

1
それほど重要ではありませんが、シリアルが増加するのはなぜですか?これを回避する方法はありませんか?
2017

1
@salient:上記で追加したように、競合のテスト前に列のデフォルト値が入力され、同時書き込みとの競合を回避するためにシーケンスがロールバックされることはありません。
Erwin Brandstetter 2017

7
信じられない。じっくりと見ればわかりやすく、魅力的な作品です。私はまだことを望みON CONFLICT SELECT...ます:)
Roshambo 2017

3
信じられない。Postgresの作成者はユーザーを拷問しているようです。なぜ単にしないの復帰に関係なく挿入があったか否かと、節が常に値を返しますか?
Anatoly Alekseev

16

アップサートは、の拡張された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)

2
影響を受ける行IDを常に取得し、それが挿入であるか、挿入であるかを知るための良い方法です。ちょうど私が必要としたもの。
Moby Duck

これはまだ「更新の実行」を使用していますが、その欠点はすでに説明されています。
ビルワージントン

4

単一のアイテムの挿入の場合、IDを返すときに、おそらく合体を使用します。

WITH new_chats AS (
    INSERT INTO chats ("user", "contact", "name")
    VALUES ($1, $2, $3)
    ON CONFLICT("user", "contact") DO NOTHING
    RETURNING id
) SELECT COALESCE(
    (SELECT id FROM new_chats),
    (SELECT id FROM chats WHERE user = $1 AND contact = $2)
);

2
WITH e AS(
    INSERT INTO chats ("user", "contact", "name") 
           VALUES ($1, $2, $3), 
                  ($2, $1, NULL) 
    ON CONFLICT("user", "contact") DO NOTHING
    RETURNING id
)
SELECT * FROM e
UNION
    SELECT id FROM chats WHERE user=$1, contact=$2;

を使用する主な目的はON CONFLICT DO NOTHING、エラーをスローしないようにすることですが、行が返されません。したがってSELECT、既存のIDを取得するには別のID が必要です。

このSQLでは、競合で失敗した場合、何も返されず、2番目SELECTは既存の行を取得します。挿入が成功した場合、同じレコードが2つUNIONあるため、結果をマージする必要があります。


このソリューションはうまく機能し、DBへの不要な書き込み(更新)を回避します!! いいね!
Simon C

0

Erwin Brandstetterによる驚くべき回答を変更しました。これは、シーケンスをインクリメントせず、行を書き込みロックしません。私はPostgreSQLに比較的慣れていないので、この方法に欠点がある場合は遠慮なくお知らせください。

WITH input_rows(usr, contact, name) AS (
   VALUES
      (text 'foo1', text 'bar1', text 'bob1')  -- type casts in first row
    , ('foo2', 'bar2', 'bob2')
    -- more?
   )
, new_rows AS (
   SELECT 
     c.usr
     , c.contact
     , c.name
     , r.id IS NOT NULL as row_exists
   FROM input_rows AS r
   LEFT JOIN chats AS c ON r.usr=c.usr AND r.contact=c.contact
   )
INSERT INTO chats (usr, contact, name)
SELECT usr, contact, name
FROM new_rows
WHERE NOT row_exists
RETURNING id, usr, contact, name

これは、テーブルのchats列に一意の制約があることを前提としています(usr, contact)

更新:spatarから提案されたリビジョンを追加しました(下記)。ありがとう!


1
CASE WHEN r.id IS NULL THEN FALSE ELSE TRUE END AS row_existsただ書く代わりにr.id IS NOT NULL as row_existsWHERE row_exists=FALSEただ書く代わりにWHERE NOT row_exists
スパター
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.