これは、 SELECTの繰り返しの問題です。 またはINSERT UPSERTに関連する(ただし異なる)同時書き込み負荷の可能性がある場合 (これは INSERT またはUPDATE 。
このPL/pgSQL関数はUPSERT(INSERT ... ON CONFLICT .. DO UPDATE )からINSERT またはSELECT 単一行 :
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int)
LANGUAGE plpgsql AS
$func$
BEGIN
SELECT tag_id -- only if row existed before
FROM tag
WHERE tag = _tag
INTO _tag_id;
IF NOT FOUND THEN
INSERT INTO tag AS t (tag)
VALUES (_tag)
ON CONFLICT (tag) DO NOTHING
RETURNING t.tag_id
INTO _tag_id;
END IF;
END
$func$;
競合状態の小さなウィンドウがまだあります。 絶対に確実にする IDを取得します:
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int)
LANGUAGE plpgsql AS
$func$
BEGIN
LOOP
SELECT tag_id
FROM tag
WHERE tag = _tag
INTO _tag_id;
EXIT WHEN FOUND;
INSERT INTO tag AS t (tag)
VALUES (_tag)
ON CONFLICT (tag) DO NOTHING
RETURNING t.tag_id
INTO _tag_id;
EXIT WHEN FOUND;
END LOOP;
END
$func$;
db<>ここでフィドル
これは、いずれかのINSERTまでループし続けます またはSELECT 成功します。電話:
SELECT f_tag_id('possibly_new_tag');
後続のコマンドが同じトランザクション内の場合 行の存在に依存しており、他のトランザクションが同時に行を更新または削除する可能性がある場合は、SELECTで既存の行をロックできます。 FOR SHAREを含むステートメント 。
代わりに行が挿入された場合、トランザクションが終了するまで、行はロックされます(または他のトランザクションでは表示されません)。
一般的なケースから始めます(INSERT vs SELECT )高速化する。
関連:
- 条件付きINSERTからIDを取得
- RETURNING from INSERT ...ONCONFLICTに除外された行を含める方法
INSERTに関連する(純粋なSQL)ソリューション またはSELECT 複数の行 (セット)一度に:
- PostgreSQLでONCONFLICTを使用してRETURNINGを使用するにはどうすればよいですか?
これの何が問題になっていますか 純粋なSQLソリューション?
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int)
LANGUAGE sql AS
$func$
WITH ins AS (
INSERT INTO tag AS t (tag)
VALUES (_tag)
ON CONFLICT (tag) DO NOTHING
RETURNING t.tag_id
)
SELECT tag_id FROM ins
UNION ALL
SELECT tag_id FROM tag WHERE tag = _tag
LIMIT 1;
$func$;
完全に間違っているわけではありませんが、@ FunctorSaladがうまくいったように、抜け穴を塞ぐことはできません。並行トランザクションが同時に同じことを行おうとすると、関数は空の結果を出す可能性があります。マニュアル:
すべてのステートメントは同じスナップショットで実行されます
並行トランザクションが少し前に同じ新しいタグを挿入したが、まだコミットしていない場合:
-
並行トランザクションが終了するのを待った後、UPSERT部分は空になります。 (同時トランザクションがロールバックする必要がある場合でも、新しいタグが挿入され、新しいIDが返されます。)
-
SELECT部分も空になります。これは、同じスナップショットに基づいており、(まだコミットされていない)同時トランザクションからの新しいタグが表示されていないためです。
何も得られない 。意図したとおりではありません。これは素朴なロジックには直観に反します(そして私はそこで捕まりました)が、PostgresのMVCCモデルが機能する方法です-機能する必要があります。
したがって、複数のトランザクションが同じタグを同時に挿入しようとする可能性がある場合は、これを使用しないでください。 または 実際に行を取得するまでループします。いずれにせよ、一般的な作業負荷でループがトリガーされることはほとんどありません。
Postgres9.4以前
この(少し簡略化された)テーブルを考えると:
CREATE table tag (
tag_id serial PRIMARY KEY
, tag text UNIQUE
);
ほぼ100%安全 新しいタグを挿入/既存のタグを選択する関数は、次のようになります。
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT tag_id int)
LANGUAGE plpgsql AS
$func$
BEGIN
LOOP
BEGIN
WITH sel AS (SELECT t.tag_id FROM tag t WHERE t.tag = _tag FOR SHARE)
, ins AS (INSERT INTO tag(tag)
SELECT _tag
WHERE NOT EXISTS (SELECT 1 FROM sel) -- only if not found
RETURNING tag.tag_id) -- qualified so no conflict with param
SELECT sel.tag_id FROM sel
UNION ALL
SELECT ins.tag_id FROM ins
INTO tag_id;
EXCEPTION WHEN UNIQUE_VIOLATION THEN -- insert in concurrent session?
RAISE NOTICE 'It actually happened!'; -- hardly ever happens
END;
EXIT WHEN tag_id IS NOT NULL; -- else keep looping
END LOOP;
END
$func$;
db<>ここでフィドル
古いsqlfiddle
なぜ100%ではないのですか?関連するUPSERTのマニュアルの注記を検討してください 例:
- https://www.postgresql.org/docs/current/plpgsql-control-structures.html#PLPGSQL-UPSERT-EXAMPLE
説明
-
SELECTをお試しください 最初 。このようにして、かなり高価なを回避できます。 99.99%の確率で例外処理。 -
CTEを使用して、競合状態の(すでに小さい)タイムスロットを最小限に抑えます。
-
SELECT間の時間枠 およびINSERT1つのクエリ内 超小型です。同時負荷が大きくない場合、または1年に1回例外が発生する可能性がある場合は、ケースを無視してSQLステートメントを使用できます。これはより高速です。 -
FETCH FIRST ROW ONLYは必要ありません (=LIMIT 1)。タグ名は明らかにUNIQUE。 -
FOR SHAREを削除します 私の例では、通常、DELETEを同時に実行しない場合 またはUPDATEテーブルのtag。わずかなパフォーマンスのコストがかかります。 -
言語名を引用しないでください:
'plpgsql'。plpgsql識別子です 。引用は問題を引き起こす可能性があり、下位互換性のためにのみ許容されます。 -
idのようなわかりにくい列名は使用しないでください またはname。いくつかのテーブルを結合するとき(これがあなたの仕事です リレーショナルDBの場合)複数の同一の名前になり、エイリアスを使用する必要があります。
関数に組み込まれています
この関数を使用すると、FOREACH LOOPを大幅に簡素化できます。 宛先:
...
FOREACH TagName IN ARRAY $3
LOOP
INSERT INTO taggings (PostId, TagId)
VALUES (InsertedPostId, f_tag_id(TagName));
END LOOP;
...
ただし、unnest()を使用した単一のSQLステートメントとしてより高速です。 :
INSERT INTO taggings (PostId, TagId)
SELECT InsertedPostId, f_tag_id(tag)
FROM unnest($3) tag;
ループ全体を置き換えます。
代替ソリューション
このバリアントは、UNION ALLの動作に基づいています。 LIMITを使用 句:十分な行が見つかるとすぐに、残りは実行されません:
- 結果が得られるまで複数のSELECTを試す方法はありますか?
これに基づいて、INSERTを外部委託できます。 別の関数に。そこでのみ、例外処理が必要です。最初の解決策と同じくらい安全です。
CREATE OR REPLACE FUNCTION f_insert_tag(_tag text, OUT tag_id int)
RETURNS int
LANGUAGE plpgsql AS
$func$
BEGIN
INSERT INTO tag(tag) VALUES (_tag) RETURNING tag.tag_id INTO tag_id;
EXCEPTION WHEN UNIQUE_VIOLATION THEN -- catch exception, NULL is returned
END
$func$;
主な機能で使用されるもの:
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int)
LANGUAGE plpgsql AS
$func$
BEGIN
LOOP
SELECT tag_id FROM tag WHERE tag = _tag
UNION ALL
SELECT f_insert_tag(_tag) -- only executed if tag not found
LIMIT 1 -- not strictly necessary, just to be clear
INTO _tag_id;
EXIT WHEN _tag_id IS NOT NULL; -- else keep looping
END LOOP;
END
$func$;
-
ほとんどの呼び出しに
SELECTのみが必要な場合、これは少し安価です。 、INSERTを使用したより高価なブロックのためEXCEPTIONを含む 句が入力されることはめったにありません。クエリも簡単です。 -
FOR SHAREここではできません(UNIONでは許可されていません クエリ)。 -
LIMIT 1必要ありません(9.4ページでテスト済み)。 PostgresはLIMIT 1を導き出しますINTO _tag_idから 最初の行が見つかるまでのみ実行されます。