Postgres UPDATE…LIMIT 1


77

サーバーステータス(「アクティブ」、「スタンバイ」など)などのサーバーのクラスターに関する詳細を含むPostgresデータベースがあります。アクティブなサーバーはいつでもスタンバイにフェールオーバーする必要があり、特にどのスタンバイが使用されているかは気にしません。

データベースクエリでスタンバイのステータス(JUST ONE)を変更し、使用するサーバーIPを返すようにします。選択は任意です。サーバーのステータスはクエリによって変化するため、どのスタンバイが選択されているかは関係ありません。

クエリを1つの更新のみに制限することはできますか?

ここに私がこれまで持っているものがあります:

UPDATE server_info SET status = 'active' 
WHERE status = 'standby' [[LIMIT 1???]] 
RETURNING server_ip;

Postgresはこれを好まない。別に何ができますか?


コードでサーバーを選択し、制約のある場所として追加するだけです。これにより、最初に追加の条件(最も古い、最新、最新のアライブ、最小負荷、同じDC、異なるラック、最小エラー)を最初にチェックすることもできます。とにかく、ほとんどのフェイルオーバープロトコルには何らかの形の決定論が必要です。
eckes

@eckesそれは面白いアイデアです。私の場合、「コードでサーバーを選択する」とは、最初にデータベースから使用可能なサーバーのリストを読み取り、次にレコードを更新することを意味します。アプリケーションの多くのインスタンスがこのアクションを実行できるため、競合状態があり、アトミック操作が必要です(または5年前)。選択は確定的である必要はありませんでした。
非常に優れた

回答:


125

同時書き込みアクセスなし

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;

もともとここには単純なサブクエリがありましたがLIMITFeikeが指摘したように、特定のクエリプランを回避できます。

プランナーは、以上のネストされたループ実行計画を生成するように選択でき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()マニュアル:

セッションレベルで取得されると、明示的に解放されるかセッションが終了するまで、アドバイザリロックが保持されます。

関連する:

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.