次のような一括挿入関数set_interactions(arg_rows text)
があります。
with inserts as (
insert into interaction (
thing_id,
associate_id, created_time)
select t->>'thing_id', t->>'associate_id', now() from
json_array_elements(arg_rows::json) t
ON CONFLICT (thing_id, associate_id) DO NOTHING
RETURNING thing_id, associate_id
) select into insert_count count(*) from inserts;
-- Followed by an insert in an unrelated table that has two triggers, neither of which touch any of the tables here (also not by any of their triggers, etc.)
(「偽の行の更新」のトリックなしに、実際の挿入の数を取得する必要があるため、このようにラップします。)
テーブルinteraction
には:
トリガーはこれを行います:
DECLARE associateId text;
BEGIN
-- Go out and get the associate_id for this thing_id
BEGIN
SELECT thing.associate_id INTO STRICT associateId FROM thing WHERE thing.id = NEW.thing_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE EXCEPTION 'Could not map the thing to an associate!';
WHEN TOO_MANY_ROWS THEN
RAISE EXCEPTION 'Could not map the thing to a SINGLE associate!'; -- thing PK should prevent this
END;
-- We don't want to add an association between an associate interacting with their own things
IF associateId != NEW.associate_id THEN
-- Insert the new association, if it doesn't yet exist
INSERT INTO associations ("thing_owner", "associate")
VALUES (associateId, NEW.associate_id)
ON CONFLICT DO NOTHING;
END IF;
RETURN NULL;
END;
interactions
とassociations
はどちらも、上記のステートメントに表示されている数を超える列はありません。
アプリケーションがset_interactions()
を呼び出すと、PostgreSQL 9.6.5からdeadlock detected
エラーが発生することがあります。 1〜100行に相当するデータを並べ替えずに呼び出す場合があります。 「競合する」バッチは、(バッチレベル全体または競合する行ごとに)同一の入力を持つ場合とそうでない場合があります。
エラーの詳細:
deadlock detected while inserting index Tuple (37605,46) in relation "associations" SQL statement INSERT INTO associations ("thing_owner", "associate") VALUES (associateId, NEW.associate_id) ON CONFLICT DO NOTHING; PL/pgSQL function aud.addfriendship() line 19 at SQL statement SQL statement "with inserts as ( insert into interaction ( thing_id, associate_id, created_time) select t->>'thing_id', t->>'associate_id', now() from json_array_elements(arg_rows::json) t ON CONFLICT (thing_id, associate_id) DO NOTHING RETURNING thing_id, associate_id ) select count(*) from inserts" PL/pgSQL function setinteractions(text) line 7 at SQL statement Process 31370 waits for ShareLock on transaction 111519214; blocked by process 31418. Process 31418 waits for ShareLock on transaction 111519211; blocked by process 31370. error: deadlock detected
たぶん、関数が1回の呼び出しで重複したデータで呼び出されている場合があると思いました。そうではありません:その代わりに、保証されたエラーON CONFLICT DO UPDATE command cannot affect row a second time
が発生します。
set_interactions()
への1,000回の呼び出しを一度に試みても、同じパラメーターで、またはthing_id
とassociate_id
の値を持つ(ペア内で異なる)同じ行のペアでさえ、デッドロックを再現できませんが、他の値もなので、PostgreSQLに到達する前に、なんらかの方法で最適化されません(関数がvolatile
とマークされているため、データベースによって最適化されるべきではありません)。これは、シングルスレッドバックエンドからのものです。しかし同時に、アプリケーション自体は、デッドロックが発生している本番環境でそのようなバックエンドを1つだけ実行します。本番データベースのフルコピーに対してこれらの1,000回の呼び出しを実行し、2番目のバックエンドからの負荷がかかっている状態で、さらにinteractions
から選択する非常に長時間実行されているクエリを介してpgAdminから実行しようとしました。彼らは文句なしに成功します。
https://rcoh.svbtle.com/postgres-unique-constraints-can-cause-deadlock 一意のインデックスに依存しないようにすることについて言及している(私が理解しているように、これはPKの意味です) )重複を挿入する場合。しかし、それはON CONFLICT DO UPDATE
の前であり、この問題を解決すると考えていました。
このクエリはどのように「ランダム」にデッドロックし、どのように修正できますか? (また、なぜ上記の方法でそれを再現できないのですか?)
_ON CONFLICT
_句は、重複キーエラーを防止できます。同じキーを入力したり、同じ行を更新したりする同時トランザクションには依然として摩擦がある可能性があります。したがって、デッドロックに対する保険ではありません。
最も重要なのは、入力行への一貫した順序と_ORDER BY
_を追加することです。注文が強制されるようにするために、結果を具体化するCTEを使用します。 (Ithinkサブクエリでも機能する必要があります。念のためです。)そうしないと、一意のインデックスに同じインデックスのタプルを入力しようとする相互に絡み合った挿入により、デッドロックが発生する可能性があります。あなたが観察した。 マニュアル:
デッドロックに対する最善の防御策は、データベースを使用するすべてのアプリケーションが複数のオブジェクトのロックを一貫した順序で確実に取得することで、デッドロックを回避することです。
また、set_interactions()
はPL/pgSQL関数であるため、これはよりシンプルで安価です。
_WITH data AS (
SELECT t->>'thing_id' AS t_id, t->>'associate_id' AS a_id
-- Or, if not type text, cast right away:
-- SELECT (t->>'thing_id')::int AS t_id, (t->>'associate_id')::int AS a_id
FROM json_array_elements(arg_rows::json) t
ORDER BY 1, 2 -- deterministic, stable order (!!)
)
INSERT INTO interaction (thing_id, associate_id, created_time)
SELECT t_id, a_id, now()
FROM data
ON CONFLICT (thing_id, associate_id) DO NOTHING;
GET DIAGNOSTICS insert_count = ROW_COUNT;
_
別のCTE、RETURNING
および別のcount(*)
は必要ありません。もっと:
トリガー機能も膨らんで見えます。エラーをキャッチせず、トランザクション全体をいずれかの方法でロールバックする例外を発生させるだけなので、ネストされたブロックは必要ありません。そして例外も無意味です。
_NO_DATA_FOUND
_の最初のEXCEPTION
は、参照整合性を適用するFK制約がある適切な多対多の設計では決して発生しません。
2つ目も無意味です-あなたは同じくらい疑っています:
-PKはこれを防ぐ必要があります
トリガー機能は、要約すると次のとおりです。
_BEGIN
-- Insert the new association, if it doesn't yet exist
INSERT INTO associations (thing_owner, associate)
SELECT t.associate_id, NEW.associate_id
FROM thing t
WHERE t.id = NEW.thing_id -- -- PK guarantees 0 or 1 result
AND t.associate_id <> NEW.associate_id -- exclude association to self
ON CONFLICT DO NOTHING;
RETURN NULL;
END
_
トリガーと関数set_interactions()
を完全に削除して、このクエリを実行するだけで、質問で確認できる便利なことをすべて実行できます。
_WITH data AS (
SELECT (t->>'thing_id')::int AS t_id, (t->>'associate_id')::int AS a_id -- asuming int
FROM json_array_elements(arg_rows::json) t
ORDER BY 1, 2 -- (!!)
)
, ins_inter AS (
INSERT INTO interaction (thing_id, associate_id, created_time)
SELECT t_id, a_id, now()
FROM data
ON CONFLICT (thing_id, associate_id) DO NOTHING
RETURNING thing_id, associate_id
)
, ins_ass AS (
INSERT INTO associations (thing_owner, associate)
SELECT t.associate_id, i.associate_id
FROM ins_inter i
JOIN thing t ON t.id = i.thing_id
AND t.associate_id <> i.associate_id -- exclude association to self
ON CONFLICT DO NOTHING
)
SELECT count(*) FROM ins_inter;
_
Now、デッドロックが発生する可能性はもうありません。もちろん、他のすべてのトランザクションも同じテーブルに同時に書き込む可能性があるため、同じ行の順序に固執する必要があります。
それが不可能な場合でも、_SKIP LOCKED
_を検討している場合は、以下を参照してください。