これは、 (またはとは異なる)に関連する(ただし異なる)同時書き込み負荷の可能性がある、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$;
デシベル<>ここでフィドル
INSERT
これは、またはSELECT
が成功するまでループし続けます。電話:
SELECT f_tag_id('possibly_new_tag');
同じトランザクション内の後続のコマンドが行の存在に依存しており、他のトランザクションがその行を同時に更新または削除する可能性がある場合は、SELECT
ステートメント内の既存の行を でロックできますFOR SHARE
。
代わりに行が挿入された場合、とにかくトランザクションが終了するまでロックされます (または他のトランザクションからは見えなくなります)。
一般的なケース ( INSERT
vs SELECT
) から始めて、高速化します。
関連している:
関連する (純粋な SQL) ソリューションINSERT
またはSELECT
複数の行(セット) を一度に:
この純粋な 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 がうまくいったように、抜け穴を塞ぐことはできません。並行トランザクションが同時に同じことを行おうとすると、関数は空の結果を出す可能性があります。マニュアル:
すべてのステートメントは同じスナップショットで実行されます
同時トランザクションが少し前に同じ新しいタグを挿入したが、まだコミットしていない場合:
何も得られません。意図したとおりではありません。これは素朴なロジックとは直感に反するものですが (そして私はそこに引っ掛かりました)、Postgres の MVCC モデルはこのように機能し、機能する必要があります。
したがって、複数のトランザクションが同時に同じタグを挿入しようとする可能性がある場合は、これを使用しないでください。または、実際に行を取得するまでループします。とにかく、一般的な作業負荷でループがトリガーされることはほとんどありません。
Postgres 9.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<>fiddle
古いsqlfiddle
なぜ100%ではないのですか? UPSERT
関連する例については、マニュアルのメモを検討してください。
説明
SELECT
最初に試してみてください。このようにして、99.99% の確率でかなり高価な例外処理を回避できます。
CTEを使用して、競合状態の (すでに小さい) タイムスロットを最小限に抑えます。
1 つのクエリ内SELECT
のと の間の時間枠は非常に小さいです。同時負荷が大きくない場合、または年に 1 回の例外に耐えることができる場合は、大文字と小文字を区別せずに SQL ステートメントを使用できます。これはより高速です。INSERT
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;
...
ただし、以下を使用した単一の SQL ステートメントとしては高速ですunnest()
。
INSERT INTO taggings (PostId, TagId)
SELECT InsertedPostId, f_tag_id(tag)
FROM unnest($3) tag;
ループ全体を置き換えます。
代替ソリューション
このバリアントはUNION ALL
withLIMIT
句の動作に基づいています: 十分な行が見つかるとすぐに、残りは決して実行されません:
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
、最初の行が見つかるまで実行されます。