2017-06-20 191 views
1

私はレコードのコレクションでPostgresの更新を行う必要があります&私はストレステストに現れたデッドロックを防止しようとしています。Postgres UPDATEをORDER BYで更新する方法は?

これに対する典型的な解決策は、レコードを特定の順序(IDなど)で更新することですが、PostgresではUPDATEのORDER BYが許可されていないようです。

私は例えば、更新を行う必要があると仮定:あなたは同時に200個のクエリを実行し、デッドロックで

UPDATE BALANCES WHERE ID IN (SELECT ID FROM some_function() ORDER BY ID); 

結果。何をすべきか?

私は一般的な解決策を探しています、それはは、カーソル機能を書くよりも、よりよい解決策がなければならないというを感じ

UPDATE with ORDER BYには好きではない場合、特定の回避策。また、より良い方法がない場合、そのカーソル機能はどのように最適に見えますか?レコードごとの更新

+0

構文... UPDATE ... FROM ...構文を試しましたか? –

+0

はい、私はUPDATEを試しました.... FROM .... SELECT ... FOR UPDATEしかし、スタックトレースの変更はありません。実際に問題がより一般的になります。 – bbozo

答えて

2

私の知る限りでは、UPDATE声明を通じて直接これを達成する方法はありません。ロック順序を保証する唯一の方法は、明示的にSELECT ... ORDER BY ID FOR UPDATE、例えばでロックを取得することです。:

UPDATE Balances 
SET Balance = 0 
WHERE ID IN (
    SELECT ID FROM Balances 
    WHERE ID IN (SELECT ID FROM some_function()) 
    ORDER BY ID 
    FOR UPDATE 
) 

これはBalancesテーブルの上にIDインデックス検索を繰り返すの欠点を持っています。あなたの簡単な例では、あなたがロッククエリの実行中(ctid system columnによって表される)物理行アドレスを取得し、それを駆動するために使用することで、このオーバーヘッドを回避することができUPDATEctid Sを使用した場合

UPDATE Balances 
SET Balance = 0 
WHERE ctid = ANY(ARRAY(
    SELECT ctid FROM Balances 
    WHERE ID IN (SELECT ID FROM some_function()) 
    ORDER BY ID 
    FOR UPDATE 
)) 

(注意してください、

残念ながら、プランナーは、狭いケースでしかctidを利用しません(あなたが探しているかどうかを知ることができます)。これは、値が一過性であるためです。 EXPLAIN出力の「Tid Scan」ノード)。単一のUPDATEステートメント内でより複雑なクエリを処理する。新しいバランスはIDと一緒some_function()で返されていた場合は、IDベースのルックアップにフォールバックする必要があります:パフォーマンスのオーバーヘッドが問題になる

UPDATE Balances 
SET Balance = Locks.NewBalance 
FROM (
    SELECT Balances.ID, some_function.NewBalance 
    FROM Balances 
    JOIN some_function() ON some_function.ID = Balances.ID 
    ORDER BY Balances.ID 
    FOR UPDATE 
) Locks 
WHERE Balances.ID = Locks.ID 

場合は、使用してに頼る必要があると思いますカーソルは次のようになります。

DO $$ 
DECLARE 
    c CURSOR FOR 
    SELECT Balances.ID, some_function.NewBalance 
    FROM Balances 
    JOIN some_function() ON some_function.ID = Balances.ID 
    ORDER BY Balances.ID 
    FOR UPDATE; 
BEGIN 
    FOR row IN c LOOP 
    UPDATE Balances 
    SET Balance = row.NewBalance 
    WHERE CURRENT OF c; 
    END LOOP; 
END 
$$ 
2

一般に、並行処理は困難です。特に200個のステートメント(私はあなたがquery = SELECTだけでなく、トランザクションでさえあると仮定しています)(実際に発行されたステートメントはすべてトランザクション内にラップされています。

  1. は、デッドロックが発生する可能性があることに注意してくださいアプリケーションでそれらをキャッチし、class 40または40P01ためError Codesをチェックして、トランザクションを再試行する:

    一般的な解決策のコンセプトは、(の組み合わせ)これらのです。

  2. リザーブロック。 SELECT ... FOR UPDATEを使用してください。可能な限り明示的なロックを避けてください。ロックにより、他のトランザクションはロックの解放を待たされるため、並行性は損なわれますが、トランザクションはデッドロックになることはありません。 13章のデッドロックの例を参照してください。特に、トランザクションAがBとBを待っているもの(銀行口座のこと)。

  3. 別のIsolation Levelを選択します。たとえば、可能であればREAD COMMITEDのような弱いものを選択します。 READ COMMITEDモードではLOST UPDATEに注意してください。 REPEATABLE READでそれらを防ぐ。

ロックを使用して、すべてのトランザクションで同じ順序(たとえば、アルファベット順のテーブル名)で文を書き込んでください。

LOCK/USE A -- Transaction 1 
LOCK/USE B -- Transaction 1 
LOCK/USE C -- Transaction 1 
-- D not used -- Transaction 1 

-- A not used -- Transaction 2 
LOCK/USE B -- Transaction 2 
-- C not used -- Transaction 2 
LOCK/USE D -- Transaction 2 

一般ロック注文A B C Dです。この方法では、トランザクションは相対的な順序でインターリーブされ、デッドロックしないようにすることができます(ステートメントによっては、他のシリアル化の問題があるかもしれません)。トランザクションの文は、指定された順序で実行されますが、トランザクション1が最初の2を実行し、次にxact 2が最初のトランザクションを実行し、次に1が終了し、最後にxact 2が終了する可能性があります。

また、複数の行を含むステートメントは、同時に発生する状況では原子的には実行されないことに注意してください。あなたは二つの文Aと複数の行を含むBを持っている言い換えれば、それらはこの順序で実行することができます。

a1 b1 a2 a3 a4 b2 b3  

ではなく、Bさんが続いていますのブロックとして。 同じことがサブクエリを持つ文にも適用されます。 EXPLAINを使用してクエリプランを調べましたか?あなたのケースでは

可能ならば、あなたは何をしたいかによって

UPDATE BALANCES WHERE ID IN (
SELECT ID FROM some_function() FOR UPDATE -- LOCK using FOR UPDATE 
-- other transactions will WAIT/BLOCK temporarily on conc. write access 
); 

を試すことができ、あなたも待つことによって失われた並行性を取り戻すために、すでにロックされたデータを、スキップれる、SELECT ... FOR UPDATE SKIP LOCKを使用することができますロックを解放する別のトランザクション(FOR UPDATE)。しかし、アプリケーション・ロジックに必要なロックされた行にUPDATEは適用されません。それで後でそれを実行してください(ポイント1を参照)。

SKIP LOCKEDについてLOST UPDATESKIP LOCKEDについてLOST UPDATEをお読みください。リレーショナルDBMSは待ち行列ではありませんが、待ち行列はあなたの場合の考え方です。SKIP LOCKED参照で完全に説明されています。

HTH

+0

ありがとう、私はこれで遊ぶよ。いくつかのロックが選択のためのフードの下で行われていますか?私は思っていませんが、私は夢中になります – bbozo

+1

あなたの 'FOR UPDATE'は何もしていないと思います。なぜなら、' SELECT'はテーブルを参照していないからです... –

関連する問題