sql >> データベース >  >> RDS >> PostgreSQL

PostgreSQLでONCONFLICTを使用してRETURNINGを使用するにはどうすればよいですか?

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

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

    • 発火すべきではないトリガーを発動する可能性があります。

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

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

    • 最も重要なこと 、PostgreSQLのMVCCモデルでは、UPDATEごとに新しい行バージョンが書き込まれます 、行データが変更されたかどうかに関係なく。これにより、UPSERT自体のパフォーマンスの低下、テーブルの肥大化、インデックスの肥大化、テーブルでの後続の操作のパフォーマンスの低下、VACUUMが発生します。 費用。重複が少ない場合のマイナーな影響ですが、大規模 主にデュープ用。

    プラスON CONFLICT DO UPDATEを使用することが実用的でない場合や不可能な場合もあります 。マニュアル:

    ON CONFLICT DO UPDATEの場合 、 conflict_target 提供する必要があります。

    シングル 複数のインデックス/制約が含まれている場合、「競合ターゲット」は使用できません。ただし、複数の部分インデックスに関連するソリューションは次のとおりです。

    • NULL値のUNIQUE制約に基づくUPSERT

    トピックに戻ると、空の更新や副作用なしで(ほぼ)同じことを達成できます。次のソリューションの一部は、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による重複 (定義上、完全なインデックスが存在するため、安価であるはずです。インデックスを使用して一意の制約が実装されます)。

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

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

    デフォルト値はbeforeに入力されるため、添付されたシーケンスは引き続き拡張されます。 競合のテスト。

    CTEについて:

    • SELECTタイプのクエリは、ネストできる唯一のタイプですか?
    • リレーショナル分割でSELECTステートメントを重複排除する

    同時書き込みロードあり

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

    • 同時トランザクションにより、挿入に固有の制約がある競合状態が発生します

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

    並行性の問題1

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

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

    他のトランザクションが正常に終了した場合(暗黙的または明示的なCOMMIT )、INSERT 競合を検出します(UNIQUE インデックス/制約は絶対です)およびDO NOTHING 、したがって、行も返しません。 (同時実行の問題2 に示されているように、行をロックすることもできません。 以下、表示されていない 。)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;
    

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

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

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

    並行性の問題2

    同時トランザクションが影響を受ける行の関連する列に書き込むことができ、同じトランザクションの後の段階で見つかった行がまだそこにあることを確認する必要がある場合は、既存の行をロックすることができます。 CTEのinsで安く (そうでなければロックが解除されます)with:

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

    そして、SELECTにロック句を追加します 同様に、FOR UPDATEのように 。

    これにより、競合する書き込み操作は、すべてのロックが解放されるトランザクションの終了まで待機します。簡潔にしてください。

    詳細と説明:

    • RETURNING from INSERT ...ONCONFLICTに除外された行を含める方法
    • 関数内のSELECTまたはINSERTは競合状態になりやすいですか?

    デッドロック?

    デッドロックから身を守る 一貫した順序で行を挿入する 。参照:

    • 競合が発生していないにもかかわらず、複数行のINSERTでデッドロックが発生する

    データ型とキャスト

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

    独立した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')
       )
       ...
    

    これは、一部のデータ型では機能しません。参照:

    • 複数の行を更新するときにNULLタイプをキャストする

    ...と名前

    これはすべてでも機能します データ型。

    テーブルのすべての(先頭の)列に挿入するときに、列名を省略できます。テーブル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に置き換えました 。



    1. MariaDBMaxScale2.4の新機能

    2. MySQLの「groupby」の「最後の」行を返す

    3. Oracle:シーケンスMySequence.currvalはこのセッションではまだ定義されていません

    4. Where句の不明な列