web-dev-qa-db-ja.com

Postgres UPDATEとORDER BY、それを行う方法は?

レコードのコレクションに対してPostgresの更新を行う必要があり、ストレステストで表示されたデッドロックを回避しようとしています。

これに対する典型的な解決策は、たとえばIDによって特定の順序でレコードを更新することです-しかし、PostgresはUPDATEにORDER BYを許可していないようです。

たとえば、更新を行う必要があると仮定します。

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

200クエリを同時に実行すると、デッドロックが発生します。何をすべきか?

PDATE with ORDER BY のようなケース固有の回避策ではなく、一般的な解決策を探しています

カーソル関数を作成するよりも優れた解決策が必要であると感じます。また、より良い方法がない場合、そのカーソル機能はどのように最適に見えますか?レコードごとに更新

13
bbozo

私の知る限り、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
)
_

これには、IDテーブルでBalancesインデックス検索を繰り返すという欠点があります。簡単な例では、ロッククエリ中に物理行アドレス( ctid system column で表される)をフェッチし、それを使用してUPDATE

_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
))
_

ctidsを使用するときは注意してください。値は一時的なものです。ここでは、ロックによって変更がブロックされるため、安全です。)

残念ながら、プランナーは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
$$
_
13
Nick Barnes

一般に、並行性は困難です。特に200のステートメント(クエリ= SELECTだけではないことを前提としています)またはトランザクション(実際には、発行されたすべてのステートメントは、トランザクション内にない場合はトランザクションにラップされます)。

一般的なソリューションの概念は、これらの(の組み合わせ)です。

  1. デッドロックが発生する可能性があることを認識するには、アプリケーションでキャッチし、_ エラーコードclass 40または40P01を確認して、トランザクションを再試行します。

  2. ロックを予約します。 SELECT ... FOR UPDATEを使用します。明示的なロックをできるだけ長く回避します。ロックは、他のトランザクションにロックの解放を強制的に待機させます。これにより、並行性が損なわれますが、トランザクションがデッドロックに陥るのを防ぐことができます。第13章のデッドロックの例を確認してください。特に、トランザクションAがBを待機し、BがA(銀行口座のこと)を待機するものを確認します。

  3. 別の 分離レベル を選択します。たとえば、可能であればREAD COMMITEDなどの弱いものを選択します。 LOST UPDATEモードのREAD COMMITEDsに注意してください。 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が終了する場合があります。

また、複数の行を含むステートメントは、並行状況ではアトミックに実行されないことを理解する必要があります。つまり、複数の行を含む2つのステートメントAとBがある場合、それらは次の順序で実行できます。

a1 b1 a2 a3 a4 b2 b3     

ただし、aの後に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を参照)。

LOST UPDATE についてLOST UPDATEおよび SKIP LOCKED についてSKIP LOCKEDもお読みください。リレーショナルDBMSはキューではありませんが、キューはSKIP LOCKEDのリファレンスで完全に説明されている場合の考えです。

HTH

2
flutter