PostgreSQL での重複更新時に挿入しますか?
-
12-09-2019 - |
質問
数か月前、私は Stack Overflow の回答から、次の構文を使用して 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 ドキュメントのどこでこれがカバーされているかわかりません。
明確にするために、いくつかのものを挿入し、それらがすでに存在する場合はそれらを更新したいと思います。
解決
PostgreSQL バージョン 9.5 以降では、 アップサート 構文、 紛争中 句。 次の構文を使用します (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 さんの答え より明確な例として。
他のヒント
の警告:同時に複数のセッションから実行された場合、これは安全ではありませんの(下記の注意事項を参照してください)。
<時間>PostgreSQLで「UPSERT」を行う別の巧妙な方法は、各何の効果も成功しないか、持っているように設計されている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は、それ以外の場合は影響を及ぼさない、成功します。
「ID = 3」の行が存在しない場合にのみます。挿入が成功します
あなたは、単一の文字列にこれら二つを組み合わせることができ、アプリケーションから実行する単一のSQL文でそれらの両方を実行します。単一のトランザクションでそれらを一緒に実行することを強くお勧めします。
これは、単独で、またはロックされたテーブル上で実行したときに非常によく動作しますが、それは行が同時に挿入されている場合は、まだ重複キーエラーで失敗することがあります、または行挿入されていない行で終了することがあり、その意味で条件をレースすることがあります同時に削除されます。 PostgreSQLの9.1以上のSERIALIZABLE
トランザクションを使用すると、多くのことを再試行する必要がありますを意味し、非常に高い直列化の失敗率のコストで確実にそれを処理します。これは、なぜこれほど複雑なにアップサートされて参照してください。より詳細にこのケースについて説明します。
read committed
ことを確認しない限り、このアプローチはまた、insert
分離に失われたアップデートにhref="https://dba.stackexchange.com/q/78510/7788">被写体をupdate行に影響を与えます。
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 によるフォローアップ
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 UPDATE
の中へON CONFLICT IGNORE
特定の値の場合)挿入提案された値は行変数として使用できます。
EXCLUDED
, 、ターゲットテーブルと同じ構造を持ちます。テーブル名を使用すると、テーブル内の元の値を取得できます。したがって、この場合にはEXCLUDED.c
になるだろう10
(それが私たちが挿入しようとしたものだからです)そして"table".c
になるだろう3
それがテーブル内の現在の値だからです。どちらかまたは両方を使用できます。SET
表現とWHERE
句。
upsert の背景については、を参照してください。 UPSERT (マージ、インサート...) の方法ON DUPLICATE UPDATE) PostgreSQL で?
私がここに来たとき、私は同じことを探していましたが、一般的な「アップサート」機能の欠如はので、私はその関数の引数は、マニュアルを形成してあなただけの更新を渡すと、SQLを挿入できると思っていた私に少しbotherd
それは次のようになります:
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;
$$;
と、おそらくあなたが最初にやりたいことを行うために、バッチ「アップサート」、あなたはSQL_UPDATE、ループ個々の更新プログラムを分割するのTclを使用することができ、preformanceのヒットは非常に小さく参照してくださいになります<のhref = "のhttp:// archives.postgresql.org/pgsql-performance/2006-04/msg00557.php」のrel = "nofollowをnoreferrer"> http://archives.postgresql.org/pgsql-performance/2006-04/msg00557.phpする
最高のコストは、実行コストがはるかに小さい。
はデータベース側では、あなたのコードからクエリを実行していますそれを行うための簡単なコマンドはありません。
最も正しいアプローチは、次のような関数を使用することです。 ドキュメント.
別の解決策 (それほど安全ではありませんが) は、return を使用して更新を実行し、どの行が更新されたかを確認し、残りの行を挿入することです。
次のようなもの:
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');
もちろん、ここには明らかな競合状態があるため、(同時環境では) 遅かれ早かれ救済されますが、通常は機能します。
ここにあります トピックに関するより長く、より包括的な記事.
個人的に、私は、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例外が生成されますよう、失敗する可能性を秘めています。しかし、非終了トランザクションは継続して成功すると、あなただけの終了トランザクションを繰り返す必要があります。
すべての時間が起きインサートのトンがある場合は、しかし、あなたは、INSERT文の周りにテーブルロックを置くことになるでしょう:SHAREのROW EXCLUSIVEロックは、ターゲット表に挿入、削除または更新行可能性のある操作を防ぐことができます。しかし、あなたは何も動作がこれを行いませんので、もし一意のキーを更新しない更新は、安全である、代わりに勧告的ロックを使用します。
また、COPYコマンドを使用すると、COPYを挿入しているので、もし、あなたが代わりにトリガーを使用する必要があります、ルールを使用しません。
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 つ入れることが重要です
- 速度を確認してください...
最も気に入った答えに似ていますが、わずかに速く動作します:
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)
私は名前と値のペアとしてのアカウント設定を管理するための同じ問題を持っています。 設計基準が異なるクライアントは、異なる設定セットを持っている可能性があることです。
JWPに似て私のソリューションは、アプリケーション内のマージレコードを生成し、バルク消去と交換することです。
これはかなり防弾、プラットフォームに依存しないで、クライアントあたり約20の設定よりも多くは決して存在しないため、これが唯一の3かなり低い負荷デシベルの呼び出しである - 。おそらく最速の方法
個々の行を更新する代替 - [例外をチェック挿入する - あるいはいくつかの組み合わせは恐ろしいコードは、低速であり、多くの場合、壊れる(上記のような)非標準の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
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
私はこの機能を使用するマージ
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
小さなセットを結合する場合は、上記の関数を使用しても問題ありません。ただし、大量のデータを結合する場合は、次のことを検討することをお勧めします。 http://mbk.projects.postgresql.org
私が知っている現在のベストプラクティスは次のとおりです。
- 新しい/更新されたデータを一時テーブルにコピーします (もちろん、コストが問題なければ INSERT を実行することもできます)
- ロックの取得 [オプション] (テーブル ロックよりも勧告の方が望ましい、IMO)
- マージ。(楽しい部分)
UPDATEが変更された行の数を返します。あなたはJDBC(Javaの)を使用している場合、あなたはそれから何行ではなく、火災のINSERTに影響を与えるされていない場合は、0に対してこの値をチェックすることができます。あなたには、いくつかの他のプログラミング言語を使用している場合は、おそらく変更された行の数はまだ得ることができ、マニュアルを参照してください。
これは、エレガントではないかもしれないが、あなたは、呼び出し元のコードから使用することがより簡単ですはるかに簡単なSQLを持っています。あなたがPL / PSQLで10行のスクリプトを記述する場合と異なり、あなたはおそらく一人でちょうどそれのために1のユニットテストや他の種類を持っている必要があります。
の編集:予想通りのこれは動作しません。 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');