現在受け入れ答えは、単一の紛争の対象、いくつかの競合、小さなタプルなしトリガの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
...
そして、同様にロッキング句を追加します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
。
ON CONFLICT UPDATE
行に変更があるように使用します。その後、それRETURNING
をキャプチャします。