挿入、PostgreSQLの重複更新時?


645

数か月前に、次の構文を使用してMySQLで複数の更新を一度に実行する方法をStack Overflowの回答から学びました。

INSERT INTO table (id, field, field2) VALUES (1, A, X), (2, B, Y), (3, C, Z)
ON DUPLICATE KEY UPDATE field=VALUES(Col1), field2=VALUES(Col2);

これでPostgreSQLに切り替えましたが、明らかにこれは正しくありません。それはすべての正しいテーブルを参照しているので、使用されているさまざまなキーワードの問題であると思いますが、PostgreSQLのドキュメントのどこでこれがカバーされているのかわかりません。

明確にするために、いくつかのものを挿入し、それらがすでに存在する場合はそれらを更新します。


38
この質問を見つけた人は、Depeszの記事「なぜ upsert がこんなに複雑なのか」を読んでください。それは問題と可能な解決策を非常によく説明しています。
クレイグリンガー

8
UPSERTはPostgresの9.5で追加されます。wiki.postgresql.org/wiki/...
tommed

4
@tommed -それが行われている:stackoverflow.com/a/34639631/4418を
ウォーレン

回答:


515

バージョン9.5以降のPostgreSQLには、ON CONFLICT句を使用したUPSERT構文があります次の構文を使用(MySQLに類似)

INSERT INTO the_table (id, column_1, column_2) 
VALUES (1, 'A', 'X'), (2, 'B', 'Y'), (3, 'C', 'Z')
ON CONFLICT (id) DO UPDATE 
  SET column_1 = excluded.column_1, 
      column_2 = excluded.column_2;

postgresqlのメールグループアーカイブで「upsert」を検索すると、マニュアルで、実行したいことの例が見つかります

例38-2。UPDATE / INSERTの例外

この例では、例外処理を使用して、必要に応じてUPDATEまたはINSERTを実行します。

CREATE TABLE db (a INT PRIMARY KEY, b TEXT);

CREATE FUNCTION merge_db(key INT, data TEXT) RETURNS VOID AS
$$
BEGIN
    LOOP
        -- first try to update the key
        -- note that "a" must be unique
        UPDATE db SET b = data WHERE a = key;
        IF found THEN
            RETURN;
        END IF;
        -- not there, so try to insert the key
        -- if someone else inserts the same key concurrently,
        -- we could get a unique-key failure
        BEGIN
            INSERT INTO db(a,b) VALUES (key, data);
            RETURN;
        EXCEPTION WHEN unique_violation THEN
            -- do nothing, and loop to try the UPDATE again
        END;
    END LOOP;
END;
$$
LANGUAGE plpgsql;

SELECT merge_db(1, 'david');
SELECT merge_db(1, 'dennis');

ハッカーのメーリングリストに、9.1以上でCTEを使用してこれを一括で行う方法の例がある可能性があります

WITH foos AS (SELECT (UNNEST(%foo[])).*)
updated as (UPDATE foo SET foo.a = foos.a ... RETURNING foo.id)
INSERT INTO foo SELECT foos.* FROM foos LEFT JOIN updated USING(id)
WHERE updated.id IS NULL;

より明確な例については、a_horse_with_no_nameの回答を参照してください。


7
これについて私が気に入らない唯一のことは、各アップサートがデータベースへの独自の個別の呼び出しになるため、処理速度が大幅に低下することです。
baash05 2012年

@ baash05それを一括で行う方法があるかもしれません、私の最新の答えを見てください。
Stephen Denne

2
私が別の方法で行う唯一のことは、LOOPの代わりにFOR 1..2 LOOPを使用することです。これにより、他の一意の制約に違反した場合に無限にスピンしないようにします。
olamork 2013

2
excludedここの最初のソリューションでは何を参照していますか?
ichbinallen

2
ドキュメント @ichbinallen ON CONFLICT DO UPDATEのSET句とWHERE句は、テーブルの名前(またはエイリアス)を使用して既存の行、および特別な除外されたテーブルを使用して挿入が提案された行にアクセスできます。この場合、特別なexcludedテーブルを使用すると、最初にINSERTしようとしていた値にアクセスできます。
TMichel

429

警告:複数のセッションから同時に実行された場合、これは安全ではありません(以下の警告を参照)。


postgresqlで「UPSERT」を実行するもう1つの賢い方法は、成功するか効果がないようにそれぞれ設計された2つの順次UPDATE / INSERTステートメントを実行することです。

UPDATE table SET field='C', field2='Z' WHERE id=3;
INSERT INTO table (id, field, field2)
       SELECT 3, 'C', 'Z'
       WHERE NOT EXISTS (SELECT 1 FROM table WHERE id=3);

「id = 3」の行がすでに存在する場合、UPDATEは成功します。それ以外の場合は、効果がありません。

INSERTは、「id = 3」の行がまだ存在しない場合にのみ成功します。

これら2つを1つの文字列に結合し、アプリケーションから1つのSQLステートメントを実行して両方を実行できます。これらを1つのトランザクションで一緒に実行することを強くお勧めします。

これは単独で、またはロックされたテーブルで実行すると非常にうまく機能しますが、行が同時に挿入されると重複キーエラーで失敗するか、行が同時に削除されると行が挿入されずに終了する可能性があるという競合状態の影響を受けます。SERIALIZABLEPostgreSQLの9.1以上の取引では、多くのことを再試行する必要がありますを意味し、非常に高い直列化の失敗率のコストで確実にそれを処理します。upsertがそれほど複雑である理由を参照してください。このケースでは、このケースについて詳しく説明しています。

このアプローチでもあるで失われたアップデート対象read committedのアプリケーションをチェックしない限り、影響を受けた行数及び検証のいずれかの単離insert、またはupdate影響を受けた行


6
短い答え:レコードが存在する場合、INSERTは何もしません。長い答え:INSERTのSELECTは、where句の一致と同じ数の結果を返します。これは最大で1(1が副選択の結果にない場合)、それ以外の場合は0です。したがって、INSERTは1行または0行を追加します。
Peter Becker

3
「ここで」部分は使用することによって簡略化することができるが存在する:... where not exists (select 1 from table where id = 3);
ENDY Tjahjono

1
これは正しい答えです。いくつかのマイナーな調整があれば、大量の更新を行うために使用できます..フム..一時テーブルを使用できるかどうか疑問に思います..
baash05

1
@ keaplogik、9.1の制限は、別の回答で説明されている書き込み可能なCTE(共通テーブル式)に関するものです。この回答で使用される構文は非常に基本的であり、長い間サポートされてきました。
ウシの

8
警告、行数がゼロでないかread committedどうかをアプリケーションが確認しない限り、これは更新が失われる可能性があります。dba.stackexchange.com/q/78510/7788を参照してくださいinsertupdate
Craig Ringer

227

PostgreSQL 9.1では、これは書き込み可能なCTE(共通テーブル式)を使用して実現できます。

WITH new_values (id, field1, field2) as (
  values 
     (1, 'A', 'X'),
     (2, 'B', 'Y'),
     (3, 'C', 'Z')

),
upsert as
( 
    update mytable m 
        set field1 = nv.field1,
            field2 = nv.field2
    FROM new_values nv
    WHERE m.id = nv.id
    RETURNING m.*
)
INSERT INTO mytable (id, field1, field2)
SELECT id, field1, field2
FROM new_values
WHERE NOT EXISTS (SELECT 1 
                  FROM upsert up 
                  WHERE up.id = new_values.id)

これらのブログエントリを参照してください:


(注)この解決策はないということではないユニークキー違反を防ぐが、それは、失われたアップデートに対して脆弱ではありません。dba.stackexchange.comでCraig Ringerによるフォローアップを
参照してください


1
@FrançoisBeausoleil:競合状態が発生する可能性は、「try / handle例外」アプローチの場合よりもはるかに小さい
a_horse_with_no_name

2
@a_horse_with_no_name競合状態が発生する可能性がはるかに小さいことをどのように正確に意味していますか 同じレコードでこのクエリを同時に実行すると、レコードが挿入されたことをクエリが検出するまで、「重複したキー値が一意の制約に違反しています」というエラーが100%発生します。これは完全な例ですか?
Jeroen van Dijk

4
@a_horse_with_no_name upsertステートメントを次のロックでラップすると、ソリューションは同時状況で機能するようです。BEGINWORK; 共有行排他モードでテーブルmytableをロックします。<ここにアップ>; コミットメント;
Jeroen van Dijk

2
@JeroenvanDijk:ありがとう。「はるかに小さい」とは、これに対する複数のトランザクション(および変更をコミットする!)の場合、すべてが単一のステートメントであるため、更新と挿入の間の期間が短くなるということです。2つの独立したINSERTステートメントによって、常にpk違反を生成できます。テーブル全体をロックすると、テーブルへのすべてのアクセスが効率的にシリアル化されます(シリアル化可能な分離レベルでも実現できるもの)。
a_horse_with_no_name 2012年

12
挿入トランザクションがロールバックすると、このソリューションは更新が失われる可能性があります。UPDATE影響を受ける行を強制するチェックはありません。
クレイグリンガー2013年

132

PostgreSQL 9.5以降では、を使用できますINSERT ... ON CONFLICT UPDATE

ドキュメントを参照してください。

MySQL INSERT ... ON DUPLICATE KEY UPDATEは直接に言い換えることができますON CONFLICT UPDATE。どちらもSQL標準の構文ではなく、どちらもデータベース固有の拡張機能です。このために使用されなかったのには十分な理由MERGEがあります。新しい構文が単に楽しみのために作成されたのではありません。(MySQLの構文には、直接採用されなかったという問題もあります)。

例えば与えられた設定:

CREATE TABLE tablename (a integer primary key, b integer, c integer);
INSERT INTO tablename (a, b, c) values (1, 2, 3);

MySQLクエリ:

INSERT INTO tablename (a,b,c) VALUES (1,2,3)
  ON DUPLICATE KEY UPDATE c=c+1;

になる:

INSERT INTO tablename (a, b, c) values (1, 2, 10)
ON CONFLICT (a) DO UPDATE SET c = tablename.c + 1;

違い:

  • 一意性チェックに使用する列名(または一意の制約名)を指定する必要あります。それはON CONFLICT (columnname) DO

  • SETこれは通常のUPDATEステートメントであるかのように、キーワードを使用する必要があります

また、いくつかの優れた機能があります。

  • あなたは持つことができWHERE、あなたの上句をUPDATE(あなたが効果的にターンさせることON CONFLICT UPDATEON CONFLICT IGNOREある値のために)

  • 挿入候補の値はEXCLUDED、ターゲットテーブルと同じ構造を持つrow-variableとして使用できます。テーブル名を使用して、テーブルの元の値を取得できます。したがって、この場合EXCLUDED.c10(これが挿入しようとしたもので"table".cある3ため)、それがテーブルの現在の値であるためです。SET式とWHERE句のどちらかまたは両方を使用できます。

upsertの背景については、PostgreSQLでUPSERT(MERGE、INSERT ... ON DUPLICATE UPDATE)する方法を参照してください


MySQLの下で自動インクリメントフィールドにギャップが生じたため、上記のようにPostgreSQLの9.5ソリューションを調べましたON DUPLICATE KEY UPDATE。Postgres 9.5をダウンロードしてコードを実装しましたが、奇妙なことに、Postgresでも同じ問題が発生します。主キーのシリアルフィールドが連続していません(挿入と更新の間にギャップがあります)。ここで何が起こっているのでしょうか?これは正常ですか?この動作を回避する方法はありますか?ありがとうございました。
WM

@WMこれは、更新/挿入操作に固有のものです。挿入を試みる前に、シーケンスを生成する関数を評価する必要があります。このようなシーケンスは同時に動作するように設計されているため、通常のトランザクションセマンティクスは適用されませんが、サブトランザクションで生成が呼び出されず、ロールバックされなかった場合でも、シーケンスは正常に完了し、残りの操作でコミットされます。したがって、これは「ギャップレス」シーケンス実装でも発生します。DBがこれを回避できる唯一の方法は、キー生成後までシーケンス生成の評価を遅らせることです。
クレイグリンガー2016

1
独自の問題を引き起こす@WM。基本的に、行き詰まっています。しかし、serial / auto_incrementがギャップレスであることに依存している場合は、すでにバグが発生しています。等の負荷の下での再起動、クライアントのエラー半ばトランザクション、クラッシュ、あなたが決して必要があり、これまでに依存している-あなたは過渡エラーを含むロールバックに配列ギャップを持つことができますSERIAL/ SEQUENCEまたはAUTO_INCREMENTギャップを持っていません。ギャップレスシーケンスが必要な場合は、さらに複雑になります。通常はカウンターテーブルを使用する必要があります。Googleが詳しく教えてくれます。ただし、ギャップのないシーケンスはすべての挿入の同時実行性を妨げることに注意してください。
クレイグリンガー2016

@WMギャップレスシーケンスとアップサートが絶対に必要な場合は、カウンターテーブルを使用するギャップレスシーケンス実装とともに、マニュアルで説明されている関数ベースのアップサートアプローチを使用できます。BEGIN ... EXCEPTION ...エラー時にロールバックされるサブトランザクションで実行されるため、INSERT失敗するとシーケンスの増分がロールバックされます。
クレイグリンガー2016

@Craig Ringer、ありがとうございました。その自動インクリメントの主キーを放棄するだけでよいことに気付きました。私は3つのフィールドの複合プライマリを作成しましたが、私の現在の特定のニーズでは、ギャップのない自動インクリメントフィールドは実際には必要ありません。再度ありがとうございます。ご提供いただいた情報により、今後、自然で健全なDBの動作を防止するための時間を節約できます。私はそれをよく理解しています。
WM

17

私がここに来たときも同じことを探していましたが、汎用の「upsert」関数が足りないので少し気になりました。更新を渡し、SQLをその関数の引数としてマニュアルから挿入できると思いました

これは次のようになります。

CREATE FUNCTION upsert (sql_update TEXT, sql_insert TEXT)
    RETURNS VOID
    LANGUAGE plpgsql
AS $$
BEGIN
    LOOP
        -- first try to update
        EXECUTE sql_update;
        -- check if the row is found
        IF FOUND THEN
            RETURN;
        END IF;
        -- not found so insert the row
        BEGIN
            EXECUTE sql_insert;
            RETURN;
            EXCEPTION WHEN unique_violation THEN
                -- do nothing and loop
        END;
    END LOOP;
END;
$$;

そして、おそらく最初にやりたかったこと、バッチ "upsert"を行うには、Tclを使用してsql_updateを分割し、個々の更新をループします。パフォーマンスヒットは非常に小さくなります。http: //archives.postgresql.org/pgsql-を参照してください。 performance / 2006-04 / msg00557.php

最も高いコストはコードからクエリを実行することであり、データベース側では実行コストがはるかに小さくなります


3
これを再試行ループで実行するDELETE必要があり、テーブルをロックするかSERIALIZABLE、PostgreSQL 9.1以降でトランザクションを分離していない限り、コンカレントと競合する傾向があります。
クレイグリンガー2013年

13

それを行う簡単なコマンドはありません。

最も正しいアプローチは、docsのような関数を使用することです。

(安全ではありませんが)別の解決策は、戻ることで更新を行い、更新された行を確認し、残りの行を挿入することです

以下に沿ったもの:

update table
set column = x.column
from (values (1,'aa'),(2,'bb'),(3,'cc')) as x (id, column)
where table.id = x.id
returning id;

id:2が返されたと仮定:

insert into table (id, column) values (1, 'aa'), (3, 'cc');

もちろん、ここには明確な競合状態があるため、遅かれ早かれ(並行環境では)救済されますが、通常は機能します。

これは、トピックに関するより長くてより包括的な記事です。


1
このオプションを使用する場合は、更新によって何も実行されない場合でも、IDが返されることを確認してください。「Update table foo set bar = 4 where bar = 4」のようなデータベースの最適化されたクエリを見てきました。
thelem

10

個人的に、私は挿入ステートメントに添付された「ルール」を設定しました。たとえば、顧客ごとのDNSヒットを時間ごとに記録する「dns」テーブルがあるとします。

CREATE TABLE dns (
    "time" timestamp without time zone NOT NULL,
    customer_id integer NOT NULL,
    hits integer
);

更新された値で行を再挿入したり、行がまだ存在しない場合は作成したりしたいと考えていました。customer_idと時間を入力します。このようなもの:

CREATE RULE replace_dns AS 
    ON INSERT TO dns 
    WHERE (EXISTS (SELECT 1 FROM dns WHERE ((dns."time" = new."time") 
            AND (dns.customer_id = new.customer_id)))) 
    DO INSTEAD UPDATE dns 
        SET hits = new.hits 
        WHERE ((dns."time" = new."time") AND (dns.customer_id = new.customer_id));

更新:これは、unique_violation例外を生成するため、同時挿入が発生すると失敗する可能性があります。ただし、終了していないトランザクションは継続して成功するため、終了したトランザクションを繰り返すだけで済みます。

ただし、常に大量の挿入が発生している場合は、挿入ステートメントの前後にテーブルロックを設定することをお勧めします。SHAREROW EXCLUSIVEロックは、ターゲットテーブルの行を挿入、削除、または更新する操作を防止します。ただし、一意のキーを更新しない更新は安全であるため、これを行う操作がない場合は、代わりにアドバイザリロックを使用してください。

また、COPYコマンドはRULESを使用しないため、COPYで挿入する場合は、代わりにトリガーを使用する必要があります。


9

この関数マージを使用します

CREATE OR REPLACE FUNCTION merge_tabla(key INT, data TEXT)
  RETURNS void AS
$BODY$
BEGIN
    IF EXISTS(SELECT a FROM tabla WHERE a = key)
        THEN
            UPDATE tabla SET b = data WHERE a = key;
        RETURN;
    ELSE
        INSERT INTO tabla(a,b) VALUES (key, data);
        RETURN;
    END IF;
END;
$BODY$
LANGUAGE plpgsql

1
update最初に実行してから、更新された行の数を確認する方が効率的です。(Ahmadの回答を参照)
a_horse_with_no_name

8

INSERT AND REPLACEを実行する場合は、上記の「upsert」関数をカスタム化します。

`

 CREATE OR REPLACE FUNCTION upsert(sql_insert text, sql_update text)

 RETURNS void AS
 $BODY$
 BEGIN
    -- first try to insert and after to update. Note : insert has pk and update not...

    EXECUTE sql_insert;
    RETURN;
    EXCEPTION WHEN unique_violation THEN
    EXECUTE sql_update; 
    IF FOUND THEN 
        RETURN; 
    END IF;
 END;
 $BODY$
 LANGUAGE plpgsql VOLATILE
 COST 100;
 ALTER FUNCTION upsert(text, text)
 OWNER TO postgres;`

そして実行した後、次のようなことをしてください:

SELECT upsert($$INSERT INTO ...$$,$$UPDATE... $$)

コンパイラエラーを回避するために、2ドルカンマを付けることが重要です。

  • 速度をチェック...

7

最も好きな答えに似ていますが、少し速く動作します:

WITH upsert AS (UPDATE spider_count SET tally=1 WHERE date='today' RETURNING *)
INSERT INTO spider_count (spider, tally) SELECT 'Googlebot', 1 WHERE NOT EXISTS (SELECT * FROM upsert)

(ソース:http : //www.the-art-of-web.com/sql/upsert/


3
2つのセッションで同時に実行すると、どちらの更新も既存の行を表示しないため、両方の更新が0行にヒットするため、両方のクエリが挿入を発行するため、これは失敗します。
クレイグリンガー、2015年

6

アカウントの設定を名前と値のペアとして管理する場合と同じ問題があります。設計基準では、クライアントごとに異なる設定セットを使用できます。

JWPに似た私のソリューションは、一括消去と置換を行い、アプリケーション内でマージレコードを生成することです。

これはかなり弾力性があり、プラットフォームに依存せず、クライアントあたりの設定は約20を超えないため、これは3つのかなり低い負荷のdb呼び出しだけです-おそらく最速の方法です。

個々の行を更新する方法-例外をチェックしてから挿入する方法-またはこれらのいくつかの組み合わせは、(上記のように)非標準のSQL例外処理がdbからdbに変更されるため、またはリリースごとに変更されるため、動作が遅く、多くの場合は中断します。

 #This is pseudo-code - within the application:
 BEGIN TRANSACTION - get transaction lock
 SELECT all current name value pairs where id = $id into a hash record
 create a merge record from the current and update record
  (set intersection where shared keys in new win, and empty values in new are deleted).
 DELETE all name value pairs where id = $id
 COPY/INSERT merged records 
 END TRANSACTION

SOへようこそ。素敵な紹介!:-)
Don Question

1
これはと似ているREPLACE INTOためINSERT INTO ... ON DUPLICATE KEY UPDATE、トリガーを使用すると問題が発生する可能性があります。トリガーやルールを更新するのではなく、トリガーやルールを削除して挿入することになります。
cHao 2014年

5

ステートメントのPostgreSQLドキュメントにINSERTよると、ON DUPLICATE KEY大文字と小文字の処理はサポートされていません。構文のその部分は、独自のMySQL拡張です。


@Lucian MERGEもまた、実際にはOLAP操作に近いものです。説明については、stackoverflow.com / q / 17267417/398670を参照してください。同時実行のセマンティクスは定義されておらず、アップサートに使用するほとんどの人はバグを作成しているだけです。
クレイグリンガー

5
CREATE OR REPLACE FUNCTION save_user(_id integer, _name character varying)
  RETURNS boolean AS
$BODY$
BEGIN
    UPDATE users SET name = _name WHERE id = _id;
    IF FOUND THEN
        RETURN true;
    END IF;
    BEGIN
        INSERT INTO users (id, name) VALUES (_id, _name);
    EXCEPTION WHEN OTHERS THEN
            UPDATE users SET name = _name WHERE id = _id;
        END;
    RETURN TRUE;
END;

$BODY$
  LANGUAGE plpgsql VOLATILE STRICT

5

小さなセットをマージするには、上記の関数を使用すると問題ありません。ただし、大量のデータをマージする場合は、http://mbk.projects.postgresql.orgを確認することをお勧めします

私が知っている現在のベストプラクティスは次のとおりです。

  1. 新しい/更新されたデータを一時テーブルにコピーします(確かに、コストに問題がない場合はINSERTを実行できます)。
  2. ロックの取得(オプション)(テーブルロック、IMOよりも推奨)
  3. マージ。(楽しい部分)

5

UPDATEは変更された行の数を返します。JDBC(Java)を使用している場合は、この値を0に対してチェックし、影響を受けた行がない場合は、代わりにINSERTを起動できます。他のプログラミング言語を使用している場合は、変更された行の数を取得できる可能性があります。ドキュメントを確認してください。

これはエレガントではないかもしれませんが、呼び出しコードから使用するのがより簡単な、はるかに単純なSQLがあります。異なる点として、PL / PSQLで10行のスクリプトを作成する場合、おそらくそれだけのために、何らかの種類の単体テストが必要です。


4

編集:これは期待どおりに動作しません。受け入れられた回答とは異なり、これは2つのプロセスがupsert_foo同時に同時に呼び出されると、一意のキー違反を生成します。

ユーレカ!私は1つのクエリでそれを行う方法を見つけましたUPDATE ... RETURNING:影響を受ける行があるかどうかをテストするために使用します:

CREATE TABLE foo (k INT PRIMARY KEY, v TEXT);

CREATE FUNCTION update_foo(k INT, v TEXT)
RETURNS SETOF INT AS $$
    UPDATE foo SET v = $2 WHERE k = $1 RETURNING $1
$$ LANGUAGE sql;

CREATE FUNCTION upsert_foo(k INT, v TEXT)
RETURNS VOID AS $$
    INSERT INTO foo
        SELECT $1, $2
        WHERE NOT EXISTS (SELECT update_foo($1, $2))
$$ LANGUAGE sql;

UPDATE、残念ながら、これは構文エラーであるため、別の手順で行う必要があります。

... WHERE NOT EXISTS (UPDATE ...)

これで、期待どおりに動作します。

SELECT upsert_foo(1, 'hi');
SELECT upsert_foo(1, 'bye');
SELECT upsert_foo(3, 'hi');
SELECT upsert_foo(3, 'bye');

1
書き込み可能なCTEを使用する場合は、これらを1つのステートメントに結合できます。しかし、ここに掲載されているほとんどのソリューションと同様に、これは間違っており、同時更新が存在すると失敗します。
クレイグリンガー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.