webdevqa.jp.net

PostgreSQLでの重複更新時に挿入しますか?

数ヶ月前、私は以下の構文を使用してMySQLで一度に複数の更新を実行する方法についてスタックオーバーフローの答えから学びました:

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のドキュメントのどこでこれがカバーされているのかわかりません。

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

573
Teifion

バージョン9.5以降のPostgreSQLは UPSERT 構文を持ち、ON CONFLICT 句があります。 次の構文で(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の回答 を参照してください。

423
Stephen Denne

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


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文で両方を実行することができます。単一のトランザクションでそれらを一緒に実行することを強くお勧めします。

これは単独でまたはロックされたテーブルで実行されるとき非常にうまくいきますが、行が同時に挿入されるならそれがまだ重複キーエラーで失敗するかもしれないという意味で競合を受ける。 PostgreSQL 9.1以降のSERIALIZABLEトランザクションは、非常に高いシリアライゼーション失敗率を犠牲にして確実に処理します。つまり、やり直す必要があります。 なぜupsertがとても複雑なのか を見てください。

このアプローチも アプリケーションが影響を受ける行数をチェックし、insertまたはupdateが行に影響を与えることを確認しない限り、read committed分離での更新の損失を免れません .

416
bovine

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)

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


このソリューションは not で一意のキーの違反を防ぐことはできますが、更新を失っても問題はありません。
dba.stackexchange.comでCraig Ringerがフォローアップ を参照 -

219

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;

違い:

  • 一意性検査に使用する列名(または固有制約名)を must に指定してください。それがON CONFLICT (columnname) DOです

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

それはいくつかの素晴らしい機能も持っています。

  • あなたはあなたのWHEREUPDATE節を持つことができます(特定の値のためにON CONFLICT UPDATEON CONFLICT IGNOREに効果的に変えることができます)

  • 挿入提案値は、ターゲット表と同じ構造を持つ行変数EXCLUDEDとして使用できます。テーブル名を使用してテーブルの元の値を取得できます。したがって、この場合、EXCLUDED.c10になり(挿入しようとしたため)、"table".c3になります。これがテーブルの現在の値です。 SET式とWHERE節では、どちらか一方または両方を使用できます。

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

113
Craig Ringer

私はここに来たときに同じものを探していましたが、汎用の「アップサート」関数の欠如が少し気になりました。

次のようになります。

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;
$$;

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

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

17
Paul Scheltema

簡単なコマンドはありません。

最も正しいアプローチは docs からのもののようにfunctionを使うことです。

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

次の行に沿った何か

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');

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

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

12
user80168

個人的には、insert文に添付された「ルール」を設定しました。顧客ごとの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例外を発生させるので同時挿入が起こっていると失敗する可能性があります。ただし、終了していないトランザクションは継続して成功します。終了したトランザクションを繰り返す必要があります。

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

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

8
Ch'marr

もしあなたがINSERTと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... $$)

コンパイラエラーを回避するために二重ドルコンマを入れることは重要です

  • 速度を確認してください...
7
Felipe FMMobile

最もよく寄せられる回答と似ていますが、やや速く動作します。

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/

6
alexkovelsky

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

私の解決策は、JWPと同様に、一括消去して置き換え、アプリケーション内にマージレコードを生成することです。

これはかなり強固で、プラットフォームに依存しません。1クライアントあたり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
5
benno

UPDATEは変更された行数を返します。 JDBC(Java)を使用している場合は、この値を0と比較して確認し、影響を受けた行がない場合は代わりにINSERTを起動します。他のプログラミング言語を使用している場合は、変更された行数がまだ得られる可能性があるので、資料を確認してください。

これはそれほど洗練されていないかもしれませんが、呼び出し側のコードから使用するのが簡単なはるかに単純なSQLがあります。それとは異なり、PL/PSQLで10行のスクリプトを作成する場合は、おそらくそれを目的とした単体テストが必要です。

4
h22

小さな集合をマージするためには、上記の関数を使うのが良いです。しかし、大量のデータをマージしているのであれば、 http://mbk.projects.postgresql.org を検討することをお勧めします。

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

  1. 新しい/更新されたデータを一時テーブルにコピーします(確かに、またはコストが問題なければINSERTを実行できます)。
  2. ロックを取得[オプション](テーブルロックよりも推奨、IMO)
  3. マージ。 (楽しい部分)
4
jwp
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
4
Ahmad

私はこの関数mergeを使います

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
4
Mise

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

編集: これは予想通りに動作しません。一般に認められている答えとは異なり、これは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');
3
Joey Adams