web-dev-qa-db-ja.com

Postgres UPDATE ... LIMIT 1

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

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

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

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

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

Postgresはこれが好きではありません。別に何ができるでしょうか?

91

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

[〜#〜] cte [〜#〜](Common Table Expressions) で選択を具体化し、それの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 s.server_ip;
_

私はここに単純なサブクエリを最初に持っていましたが、 Feike が指摘するように、特定のクエリプランのLIMITを回避できます。

プランナは、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が具体化されると、ネストされたループの異なる反復で異なる結果を返さなくなります。

Or控えめに使用correlatedサブクエリ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 LOCKED_がPostgresに追加されました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

関連:

PostgreSQLで_SKIP LOCKED_なし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は(少し意外にも)行を返しません。何も起こりません。すべてのトランザクションがthesamerowを更新したい場合、これは望ましい動作です。
ただし、各トランザクションが更新する必要がある場合はthenextrowarbitrary(またはrandom)行を更新したいだけなので、待つのは全く意味がありません。

アドバイザリロック を使用して、状況のブロックを解除できます。

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

このようにして、まだロックされていない次の行が更新されます。各トランザクションは、処理する新しい行を取得します。私はこのトリックのために Czech Postgres Wiki の助けを借りました。

idは任意の一意のbigint列(または_int4_または_int2_などの暗黙のキャストを持つ任意の型)です。

アドバイザリロックがデータベース内の複数のテーブルで同時に使用されている場合は、ここでpg_try_advisory_xact_lock(tableoid::int, id)-idを一意のintegerとして明確にします。
tableoidbigint量であるため、理論的にはintegerでオーバーフローする可能性があります。偏執狂的である場合は、代わりに_(tableoid::bigint % 2147483648)::int_を使用してください-真の妄想性のために理論的な「ハッシュ衝突」を残します...

また、PostgresはWHERE条件を任意の順序で自由にテストできます。couldpg_try_advisory_xact_lock()をテストしてロックを取得before_status = 'standby'_、 _status = 'standby'_が真でない場合、無関係な行に追加のアドバイザリロックが発生する可能性があります。 SOに関する関連質問:

通常、これは無視できます。guarantee条件を満たす行のみがロックされるようにするには、上記のような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() and pg_advisory_unlock()マニュアル:

セッションレベルで取得すると、通知ロックは明示的に解放されるかセッションが終了するまで保持されます。

関連:

138