同時書き込みアクセスなし
CTEで選択を具体化し、のFROM条項でそれに参加しUPDATEます。
WITH cte AS (
SELECT server_ip -- pk column or any (set of) unique column(s)
FROM server_info
WHERE status = 'standby'
LIMIT 1 -- arbitrary pick (cheapest)
)
UPDATE server_info s
SET status = 'active'
FROM cte
WHERE s.server_ip = cte.server_ip
RETURNING server_ip;
もともとここには単純なサブクエリがありましたがLIMIT、Feikeが指摘したように、特定のクエリプランを回避できます。
プランナーは、以上のネストされたループ実行計画を生成するように選択できLIMITing、より引き起こし、サブクエリをUPDATEsよりLIMIT:例えば、
Update on buganalysis [...] rows=5
-> Nested Loop
-> Seq Scan on buganalysis
-> Subquery Scan on sub [...] loops=11
-> Limit [...] rows=2
-> LockRows
-> Sort
-> Seq Scan on buganalysis
テストケースの再現
上記を修正する方法は、LIMITサブクエリを独自のCTEでラップすることでした。CTEは実体化されるため、ネストされたループの異なる反復で異なる結果を返しません。
または、を使用した単純なケースでは、低相関サブクエリを使用しLIMIT 1ます。よりシンプルで高速:
UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
LIMIT 1
)
RETURNING server_ip;
同時書き込みアクセス
これらすべてのデフォルトの分離レベルREAD COMMITTEDを想定しています。より厳密な分離レベル(REPEATABLE READおよびSERIALIZABLE)でも、引き続きシリアル化エラーが発生する可能性があります。見る:
同時書き込みロードの下でFOR UPDATE SKIP LOCKED、行をロックして競合状態を回避します。SKIP LOCKEDPostgres 9.5で追加されました。古いバージョンについては以下を参照してください。マニュアル:
でSKIP LOCKED、すぐにロックできない選択された行はスキップされます。ロックされた行をスキップすると、データの一貫性のないビューが提供されるため、これは汎用作業には適していませんが、キューのようなテーブルにアクセスする複数のコンシューマーとのロック競合を回避するために使用できます。
UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
LIMIT 1
FOR UPDATE SKIP LOCKED
)
RETURNING server_ip;
適格なロック解除された行が残っていない場合、このクエリでは何も起こりません(行は更新されません)。空の結果が得られます。重要でない操作の場合、これで完了です。
ただし、同時トランザクションでは行がロックされている場合がありますが、更新は完了しません(ROLLBACKまたはその他の理由)。必ず最終チェックを実行するには:
SELECT NOT EXISTS (
SELECT 1
FROM server_info
WHERE status = 'standby'
);
SELECTロックされた行も表示されます。戻りませんがtrue、1つ以上の行がまだ処理されており、トランザクションをロールバックできます。(または、新しい行その間に追加されています。)ビット、その後、ループの2つのステップを待って:(UPDATEあなたが戻って何の行を取得しないまで; SELECT...)あなたが得るまでtrue。
関連する:
SKIP LOCKEDPostgreSQL 9.4以前ではなし
UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
LIMIT 1
FOR UPDATE
)
RETURNING server_ip;
同じ行をロックしようとする並行トランザクションは、最初のトランザクションがロックを解除するまでブロックされます。
最初のトランザクションがロールバックされた場合、次のトランザクションがロックを取得して正常に続行します。キュー内の他のユーザーは待機し続けます。
最初にコミットされた場合、WHERE条件が再評価され、それTRUE以上でstatusはない(変更されている)場合、CTEは(多少驚くべきことに)行を返しません。何も起こりません。これは、すべてのトランザクションが同じ rowを更新する場合の望ましい動作です。
しかし、各トランザクションが次の行を更新したいときではありません。そして、任意の(またはランダムな)行を更新したいだけなので、まったく待つ必要はありません。
アドバイザリロックの助けを借りて、状況のブロックを解除できます。
UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
AND pg_try_advisory_xact_lock(id)
LIMIT 1
FOR UPDATE
)
RETURNING server_ip;
これにより、まだロックされていない次の行が更新されます。各トランザクションは、処理する新しい行を取得します。このトリックについては、チェコのPostgres Wikiの助けを借りました。
id任意のユニークさbigint(暗黙のようなキャストでまたは任意の型の列int4またはint2)。
勧告的ロックが同時にデータベース内の複数のテーブルのために使用されている場合は、と明確にpg_try_advisory_xact_lock(tableoid::int, id)- idユニークであることintegerここに。
ためtableoidであるbigint量は、理論的にオーバーフローすることができますinteger。あなたが十分に妄想している場合は、(tableoid::bigint % 2147483648)::int代わりに使用してください-本当にパラノイアのための理論的な「ハッシュ衝突」を残して...
また、PostgresはWHERE任意の順序で条件をテストできます。それは可能性をテスト pg_try_advisory_xact_lock()し、ロックを取得する前に、 status = 'standby'無関係な行の追加勧告的ロックにつながる可能性があり、status = 'standby'真実ではありません。SOに関する関連質問:
通常、これは無視できます。適格な行のみがロックされることを保証するには、上記のようなCTEまたはOFFSET 0ハックを使用したサブクエリ(インライン化を防止)で述語をネストできます。例:
または(シーケンシャルスキャンの場合は安い)、次のCASEようなステートメントで条件をネストします。
WHERE CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
ただし、このCASEトリックにより、Postgresでのインデックスを使用できなくなりますstatus。そのようなインデックスが利用可能な場合、最初から余分なネストを行う必要はありません。インデックススキャンでは、条件を満たす行のみがロックされます。
すべての呼び出しでインデックスが使用されていることを確認できないため、次のことができます。
WHERE status = 'standby'
AND CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
CASE論理的に冗長であるが、それのサーバー議論の目的。
コマンドが長いトランザクションの一部である場合は、手動で解放できる(および解放する必要がある)セッションレベルのロックを検討してください。だから、あなたがロックされた行で行わされるとすぐにロックを解除することができますpg_try_advisory_lock()とpg_advisory_unlock()。マニュアル:
セッションレベルで取得されると、明示的に解放されるかセッションが終了するまで、アドバイザリロックが保持されます。
関連する: