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
列を返します。
これを行うことはできますか?その場合、方法?
私はまったく同じ問題を抱えており、更新するものが何もないにもかかわらず、「何もしない」の代わりに「更新する」を使用してそれを解決しました。あなたの場合、次のようなものになります。
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;
このクエリは、挿入されたばかりか以前に存在していたかに関係なく、すべての行を返します。
現在受け入れられている回答 は、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はターゲット列からデータ型を導出できず、明示的な型キャストを追加する必要がある場合があります。 マニュアル:
VALUES
でINSERT
が使用されている場合、値はすべて、対応する宛先列のデータ型に自動的に強制変換されます。他のコンテキストで使用する場合、正しいデータ型を指定する必要がある場合があります。エントリがすべて引用符で囲まれたリテラル定数である場合、最初のものを強制するだけで、すべての想定される型を決定できます。
クエリ自体は、CTEのオーバーヘッドと追加のSELECT
により、fewdupesに対して少し高価になる可能性があります(完全なインデックスが定義により存在するため、安価であるはずです) -一意の制約がインデックスで実装されます)。
manyの複製では、(はるかに)高速になる可能性があります。追加書き込みの効果的なコストは、多くの要因に依存します。
ただし、副作用と隠れたコストが少ないがあります。おそらく全体的に安いでしょう。
(デフォルト値はbeforeでテストされ、競合のテストが行われるため、付加されたシーケンスは依然として高度です。)
CTEについて:
デフォルトのREAD COMMITTED
トランザクション分離を想定しています。
詳細な説明を含むdba.SEの関連回答:
競合状態を防ぐための最適な戦略は、正確な要件、テーブルとUPSERTの行の数とサイズ、同時トランザクションの数、競合の可能性、利用可能なリソース、その他の要因に依存します...
トランザクションが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が同じ行に書き込むトランザクションが重複している場合、デッドロックの可能性を再導入します(以下を参照)。それが問題であれば、別の解決策が必要です。
並行トランザクションが影響を受ける行の関連する列に書き込むことができ、見つかった行が同じトランザクションの後の段階でまだ存在していることを確認する必要がある場合、次の方法でlock rowsを安価に実行できます。
...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE -- never executed, but still locks the row
...
そして、 ロック節をFOR UPDATE
のようなSELECT
にも追加します 。
これにより、すべてのロックが解放されると、競合する書き込み操作がトランザクションの終わりまで待機するようになります。だから簡単に。
詳細と説明:
一貫性のある順序に行を挿入することにより、デッドロックから守ります。見る:
独立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 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
に置き換えました。
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)