sql for update徹底解説:構文とDB別ロック実践

本記事は各DBのFOR UPDATEを比較解説。WAIT/NOWAIT/SKIP LOCKEDの挙動、MySQLの共有/占有ロックと行/テーブルロック、OracleのCURRENT OF要件、Snowflakeの制限と注意点を具体例で示し、ロック待ち・競合の回避に役立ちます。

目次

SQLのFOR UPDATEとは

sql+transaction+lock

SQLのFOR UPDATEは、SELECTで取得した行に対して「これから更新する意図がある」ことを宣言し、トランザクションの間、対象行に排他的な行ロックを取得するための構文です。いわゆる悲観的ロックの代表で、同じ行を別セッションが更新・削除したり、同じくFOR UPDATEで取得したりすることを防ぎます。たとえば在庫引当、予約処理、ジョブキューの取り出しなど、同時実行時の整合性を厳密に担保したい場面で有効です。一般的にロックはCOMMITまたはROLLBACKまで保持されます。なお、sql for update を使う場合は、必ずトランザクション境界(開始・終了)を明確にすることが前提になります。

明示的に行ロックを取得する目的と効果

FOR UPDATEで明示的に行ロックを取得する主目的は、同時実行環境での「ロストアップデート」や「二重処理」を防ぎ、ビジネス上の一意性や整合性を守ることにあります。特に、数量を減算する、状態を「処理中」に遷移させる、といった競合しやすい更新では効果が大きく、アプリケーションロジックを簡潔にできます。

  • 競合の抑止: 対象行に排他ロックを取得するため、他トランザクションの更新・削除やFOR UPDATEをブロックし、ロストアップデートを防止します。
  • 一貫性の確保: SELECTした瞬間の行を、その後のUPDATE/DELETEまで一貫して扱えるため、処理途中の齟齬を回避できます。
  • 業務ユースケース: 在庫引当・予約(同一資源の二重割当防止)、採番・番号払い出し(重複防止)、ワークキューの取り出し(単一ワーカー処理の保証)など。
  • 副作用と留意点: ロックはトランザクション終了まで保持されるため、長時間トランザクションはブロッキングやデッドロックの原因になり得ます。対象行を特定できる索引を活用してスキャン範囲を狭め、必要最小限の行だけをロックすることが重要です。

まとめると、FOR UPDATEは「同時更新を確実に防ぎたい」要件に対する強力な手段ですが、ロック保持時間や検索条件の設計次第でスループットに影響するため、短いトランザクションと絞り込まれた条件での利用がベストプラクティスになります。

対応データベースとサポート状況(Oracle/MySQL/Snowflake など)

  • Oracle Database: SELECT … FOR UPDATEをネイティブにサポート。取得した行に対して排他ロックを付与し、他セッションからの更新・FOR UPDATEをブロックします。通常の読み取りは多くの場合ブロックしません。ロックはCOMMIT/ROLLBACKまで保持されます。
  • MySQL(InnoDB): トランザクション(START TRANSACTION/BEGIN)下でのSELECT … FOR UPDATEをサポート。検索でヒットした行に排他ロックを取得します。自動コミットのまま1文だけ発行すると即時解放され実用性が下がるため、明示的なトランザクション管理が前提です。ストレージエンジンはInnoDBが対象です。
  • PostgreSQL: SELECT … FOR UPDATE/SHAREをサポート。MVCC下でも更新競合を確実に抑止できます。読取り自体は基本的に非ブロッキングで、更新系の競合時に待機/競合解決が行われます。
  • Snowflake: SELECT … FOR UPDATEをサポート。MVCCベースのため通常の読み取りは影響を受けにくく、対象行の同時更新を防止する目的で利用します。利用可能な詳細オプションや制約はアカウント設定やバージョンに依存するため、公式ドキュメントの確認が推奨されます。
  • Microsoft SQL Server: SELECT … FOR UPDATEという構文はありませんが、SELECT … WITH (UPDLOCK, ROWLOCK)などのロックヒントを用いることで同等の行レベルの更新意図ロックを実現できます。
  • MariaDBなど: MySQL互換としてSELECT … FOR UPDATEをサポート(InnoDB/XtraDBなど)。挙動は概ねMySQLに準拠します。

いずれのプラットフォームでも、sql for update は「必要な行だけを短時間ロックし、整合性を優先する」ための道具です。実際のロック粒度や読み取りとの相互作用は製品固有の実装差があるため、対象データベースのドキュメントに沿って検証環境で事前確認するのが安全です。

FOR UPDATEの構文とオプション

database+locking+sql

sql for update は、SELECT で読み取った行に更新用の排他ロックを取得するための構文です。この章では、基本構文、FOR UPDATE OF 句の使い分け、待機制御(WAIT/NOWAIT/SKIP LOCKED)、さらにカーソルと CURRENT OF、DDL と WAIT の関係まで、主要オプションの挙動と注意点を整理します。

基本構文とFOR UPDATE OF句の使い方

もっとも基本的な形は、条件に一致した行を取得しつつ、その行を更新可能な排他状態にするものです。

-- 基本形(ベンダー共通イディオム)
SELECT *
FROM orders
WHERE status = 'PENDING'
FOR UPDATE;   -- ここで一致行に更新ロックを取得

結合クエリでは、どの表の行にロックを取るかを明示できます。書き方はデータベースによって異なります。

  • Oracle系: OF に「カラム」を指定して、結果として「そのカラムを持つ表の行」をロック対象にする
  • PostgreSQL系: OF に「表名」を指定して、その表の行だけロックする
  • MySQL: FOR UPDATE は行ロックを取得するが OF は非対応(8.0 では NOWAIT/SKIP LOCKED は対応)。結合時は更新対象のテーブルに対してロックがかかる実行計画になる
-- Oracle の例(customers の行のみロック)
SELECT o.order_id, c.customer_id
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
WHERE o.status = 'PENDING'
FOR UPDATE OF c.customer_id;

-- PostgreSQL の例(orders の行のみロック)
SELECT o.order_id, c.customer_id
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
WHERE o.status = 'PENDING'
FOR UPDATE OF o;

カラムを指定するFOR UPDATE OFでロック範囲を最小化

Oracle の sql for update では「FOR UPDATE OF カラム名」でロック対象を表レベルで絞り込めます。これにより、結合相手の表まですべてロックしてしまう過剰ロックを防ぎ、競合を最小化できます。

-- Oracle: 支払更新時に orders の行だけをロック(customers にはロックを広げない)
SELECT o.order_id, o.amount, c.customer_name
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
WHERE o.order_id = :id
FOR UPDATE OF o.amount, o.status;  -- 指定したカラムを持つ「orders」の行のみロック
  • 注意: 「カラムだけがロックされる」わけではありません。指定カラムを含む「行」全体がロック対象になります。
  • 指定できるのは基礎表のカラム。式や集計列は不可。ビュー経由の場合は基礎表のカラムに解決できる必要があります。
  • 複数表が関与する場合、不要な表を OF から外すことでロック競合を抑制できます。

待機制御オプションの整理

ロック済み行に対して待つか即座に返すか、またはロック済み行を飛ばすかは、オプションで制御できます。

  • WAIT n: 最大 n 秒待って取得(代表例: Oracle)。タイムアウトでエラー。
  • NOWAIT: 待たずに即エラー(Oracle、PostgreSQL、MySQL 8.0)。
  • SKIP LOCKED: ロック済み行をスキップして取得可能な行だけ返す(Oracle、PostgreSQL、MySQL 8.0)。

同じ記述でも対応可否や厳密な挙動は製品差があります。必要に応じて各製品のリファレンスで確認してください。

WAITとNOWAITの選び方(待ちの有無を制御)

ユーザー待ち時間を抑えたい運用では NOWAIT、業務上どうしても該当行を取得したい場合は WAIT が有効です。

-- Oracle: 最大 5 秒だけ待つ
SELECT *
FROM tasks
WHERE task_id = :id
FOR UPDATE WAIT 5;

-- PostgreSQL: 待たずに即時失敗させる
SELECT *
FROM tasks
WHERE task_id = $1
FOR UPDATE NOWAIT;

-- MySQL 8.0: 待たずに即時失敗させる
SELECT *
FROM tasks
WHERE task_id = ?
FOR UPDATE NOWAIT;
  • NOWAIT はキュー処理や API の短時間タイムアウトに適します。
  • WAIT n はバッチ処理や一括更新など、「待てば進む」見込みがあるときに有効です。
  • タイムアウト到達時はエラーとなるため、アプリ側でリトライ戦略を設計してください。

SKIP LOCKEDでロック済み行をスキップする

同時実行ワーカーでの「取り合い」を避け、空いている仕事だけ取り出す用途で威力を発揮します。典型的にはワークキューやメッセージ取り出しに使います。

-- 先に取れるジョブだけを取得(ロック済みは飛ばす)
SELECT job_id, payload
FROM job_queue
WHERE status = 'READY'
ORDER BY created_at
FOR UPDATE SKIP LOCKED
FETCH FIRST 10 ROWS ONLY;
  • 利点: デッドロックや長い待機を避けつつスループット向上。
  • 注意: 厳密な順序保証(例: created_at の完全順守)は崩れる可能性があります。必要ならアプリ側で順序制御を補完してください。
  • 取り出し後は確実に状態遷移(例: IN_PROGRESS)まで含めて同一トランザクションで行うのが通例です。

カーソルとCURRENT OFの活用

カーソルを FOR UPDATE で開き、FETCH した行を WHERE CURRENT OF で安全に更新・削除するパターンは、1 件ずつの逐次処理と相性が良い書き方です。

-- Oracle の例
DECLARE
  CURSOR c_orders IS
    SELECT order_id, amount
    FROM orders
    WHERE status = 'PENDING'
    FOR UPDATE OF amount, status;  -- 更新対象表の行をロック
  v_rec c_orders%ROWTYPE;
BEGIN
  OPEN c_orders;
  LOOP
    FETCH c_orders INTO v_rec;
    EXIT WHEN c_orders%NOTFOUND;

    UPDATE orders
      SET status = 'PROCESSING'
    WHERE CURRENT OF c_orders;  -- 直前に FETCH した行だけを特定
  END LOOP;
  CLOSE c_orders;
END;
/

-- PostgreSQL の例
BEGIN;
DECLARE c_orders CURSOR FOR
  SELECT order_id, amount
  FROM orders
  WHERE status = 'PENDING'
  FOR UPDATE;
FETCH NEXT FROM c_orders;
UPDATE orders SET status = 'PROCESSING' WHERE CURRENT OF c_orders;
COMMIT;

CURRENT OFの利用条件とFOR UPDATE OFに関する制約

  • カーソル側の SELECT が FOR UPDATE(行ロック取得)であること。
  • カーソルが「更新可能」な形であること(一般に DISTINCT、GROUP BY、集合演算、集計関数、複雑なビューなどは不可)。
  • 結合を含む場合、更新先の基礎表が一意に定まる必要があります。
  • Oracle では、FOR UPDATE OF で対象表を特定しておくと、WHERE CURRENT OF でその表を更新できます(OF に指定のない表は更新不可)。
  • トリガーや外部キー制約などによる副作用でロック順が変わる場合、デッドロック検討が必要です。

DDLとWAITの関係(スキーマ変更時の待機挙動)

SELECT … FOR UPDATE の WAIT/NOWAIT/SKIP LOCKED は「行ロックの競合」に対する待機を制御します。一方、DDL(ALTER TABLE など)は表やスキーマに対するメタデータロックを取得するため、相互に待機が発生する点に注意が必要です。

  • 典型例1: 実行中トランザクションが行ロックを保持していると、同じ表への DDL は完了まで待機(あるいはタイムアウト)する。
  • 典型例2: 大規模 DDL がメタデータロックを保持中だと、その表に対する SELECT … FOR UPDATE 自体が開始待ちになることがある。
  • 重要: 多くの製品で、FOR UPDATE の WAIT/NOWAIT はメタデータロックの待機時間を直接制御しません(行ロックに対する待機制御)。

代表的な対処の方向性(製品により名称・挙動は異なります)。

  • DDL 側の待機時間・リトライ方針を設定(例: セッションの DDL ロックタイムアウトやロックタイムアウトを設定)。
  • メンテナンス時間帯で DDL を実行し、アプリ側は FOR UPDATE を伴う長期トランザクションを避ける。
  • 大規模スキーマ変更はオンラインオペレーションや段階移行(新旧併存)を検討。
-- 例: (ベンダー例)DDL ロック待機時間の設定イメージ
-- Oracle: セッション単位で DDL ロックの待機秒数を設定
ALTER SESSION SET DDL_LOCK_TIMEOUT = 10;

-- PostgreSQL: 任意ロック取得に対するタイムアウトを設定
SET lock_timeout = '10s';

要点として、sql for update の WAIT/NOWAIT/SKIP LOCKED は「行ロック」に対する待機・スキップ戦略を決めるもの、DDL は「メタデータロック」による待機が別経路で発生し得るもの、と理解して運用計画を立てると安全です。

トランザクション運用とロック管理

sql+transaction+locking

sql for update を用いた行ロックは、トランザクションの終了まで保持されます。安定運用のためには「いつ、どのように終わらせるか」を決める終了処理の徹底、ロック競合時の待機(ブロッキング)の理解、そしてロック待ちの可視化と原因特定の手順が欠かせません。本章では、運用者・開発者が日々直面する観点に絞って要点を整理します。

UPDATEしない場合のロック解放(終了処理のベストプラクティス)

SELECT … FOR UPDATE は更新の有無に関わらず、対象行に排他ロックを取得します。つまり「更新しない」と判断した段階でも、明示的にトランザクションを終了しない限りロックは保持され、他トランザクションをブロックします。不要なブロッキングやスループット低下を避けるため、次の原則を徹底してください。

  • ロックの取得は最短距離で行う:検索→判定→(必要な場合のみ)SELECT … FOR UPDATE→即時の更新/確定の順で、ロック期間を最小化する。
  • 「更新しない」と決めたら即COMMIT/ROLLBACK:業務判定やユーザー入力待ちをロック保持中に挟まない。
  • トランザクション境界を明示する:フレームワークの暗黙トランザクションに依存せず、開始/終了をコードで制御する。
  • 例外経路でも必ず終了する:try/finally(またはdefer)でCOMMIT/ROLLBACKを保証する。接続プール返却前に終了されていることを検証する。
  • スコープを小さく保つ:1トランザクションで扱う行数・テーブル数・処理時間を最小化し、バッチはチャンクに分割する。
  • 待機時間の上限を設ける:ロック待ち/ステートメントのタイムアウトを設定し、無限待ちを禁止する。
  • 読取専用処理は分離:ロック不要な参照系は別トランザクション(またはREAD ONLY)で実行する。
  • 接続のライフサイクル管理:長時間アイドル中のトランザクション(idle in transaction)を作らない。監視で検知・強制終了の運用ルールを持つ。
// 擬似コード(言語非依存)
beginTransaction()
try {
  // ロック不要の判定処理
  const targetIds = findTargetIds(criteria)

  if (targetIds.isEmpty()) {
    commit() // 早期終了
    return
  }

  // 必要な最小集合のみロック
  const rows = select ... for update where id in (:targetIds)

  if (shouldSkipUpdate(rows)) {
    commit() // UPDATEしないと決めたら即終了
    return
  }

  // 最小の更新と即時コミット
  update ... where id in (:targetIds)
  commit()
} catch (e) {
  rollback() // 例外でも必ず解放
  throw e
} finally {
  closeResources() // カーソル/ステートメント/接続のクリーンアップ
}

ロック競合と待機の関係(ブロッキングの仕組み)

FOR UPDATE による行ロックは、同一行に対する他の更新系操作やFOR UPDATE取得を待機させます。待機はしばしば「キュー(待ち行列)」の形で連鎖し、最古のトランザクション(ヘッドブロッカー)が解放されるまで後続が進めません。代表的な流れは次の通りです。

  1. T1が行AをSELECT … FOR UPDATEし、排他ロックを保持する。
  2. T2が同じ行AのUPDATE(またはFOR UPDATE)を要求し、T1にブロックされ待機に入る。
  3. T3も行Aを更新しようとすると、T2の前にT1が存在するため、やはり待機となる(ブロッキングチェーン)。
  4. T1がCOMMIT/ROLLBACKすると、次点のT2がロックを取得し処理を再開、続いてT3…と解消される。
時間 →
T1: [LOCK A -------------- COMMIT]
T2:        [ wait for A ] [LOCK A -- COMMIT]
T3:                 [ wait for A ] [LOCK A - COMMIT]
  • キューの先頭(ヘッドブロッカー)を特定することが、復旧の最短経路です。
  • スキャンで多くの行を巻き込むと、ロック数が増え競合確率が上がります。絞り込み索引でロック対象を最小化します。
  • ロック取得順序の不一致はデッドロックの温床です。複数リソースを扱う場合は取得順序を統一します。
  • 待機ポリシー(待つ/待たない/スキップする)や待機時間の上限は、要件に合わせて明示的に設計します。

ロック待ちやブロック中クエリの確認方法

ブロッキングは「見える化」できれば怖くありません。手順と観点を標準化し、誰が・何に対して・どれくらいの時間ブロックしているかを即座に把握できる体制を用意します。

  1. 症状の把握
    • タイムアウトやスロークエリのログに「ロック待ち」原因が含まれているかを確認(エラーコード/メッセージ)。
    • レイテンシの上昇が特定テーブルや特定バッチの時間帯に偏っていないかを可視化。
  2. ブロッカーの特定
    • データベースのセッション/トランザクション/ロック監視ビューを参照し、以下の項目で突合する。
    • 確認すべきキー情報:
      • 待機中セッションIDとブロック元セッションID(who blocks whom)
      • ロック対象(テーブル/行の識別子、インデックス/キー情報)
      • 保持/待機時間(最古のトランザクションの開始時刻)
      • 実行中SQL/直前のSQL(SELECT … FOR UPDATE や UPDATE の本文)
  3. 原因の分類
    • ロングトランザクション(終了忘れ/ユーザー入力待ち)か。
    • スキャンの広さ(索引不足/クエリ条件の非選択性)によるロック拡大か。
    • 取得順序の不一致や再試行ロジックの欠如によるデッドロック多発か。
  4. 対処と再発防止
    • 即時対処:ヘッドブロッカーの完了促進、必要に応じたセッションのキャンセル/切断(業務影響と整合性を確認)。
    • 恒久対策:トランザクションの短縮、インデックス整備、ロック取得順序の統一、適切な待機ポリシー/タイムアウト設定、監視アラートの自動化。
-- ベンダごとに名称は異なるが、概ね次の観点のビュー/関数が提供されている
-- 1) セッション/接続一覧(状態、実行SQL、経過時間)
-- 2) トランザクション一覧(開始時刻、分離レベル、ロック待ちフラグ)
-- 3) ロック一覧(保持側と待機側、対象オブジェクト)
-- 4) ブロック関係(waiting_session → blocking_session の対応)
-- これらを結合して「最古のヘッドブロッカー」と「影響範囲(待機セッション数)」を抽出する。
  • 運用ダッシュボードでは、次のメトリクスを常時可視化すると効果的です:
    • ロック待ち時間(p50/p95/p99)と件数
    • 最古トランザクションの経過時間
    • ブロック中セッション数と最大チェーン長
    • タイムアウト件数(ロック/ステートメント)

「短いトランザクション」「最小ロック」「速やかな終了」を徹底し、ロック監視と分析を日常運用に組み込むことが、FOR UPDATE を活用した高スループット設計の近道です。

ロックの基礎知識と動作検証

sql+for+update

トランザクションの整合性を守るうえで、ロックの仕組みを正しく理解することは不可欠です。特に、sql for update(SELECT … FOR UPDATE)で明示的に行ロックを取得する場面では、共有ロックと排他ロックの互換性、ロックの粒度(行ロック/テーブルロック)が実行計画やスループットに与える影響を把握しておく必要があります。ここでは概念整理に加えて、セッションを分けた動作検証の観点で具体的な挙動を確認します。

排他ロック(占有)と共有ロックの違い

ロックは大きく「共有ロック(S: Shared)」と「排他ロック(X: Exclusive)」に分類できます。共有ロックは同一リソースに対する読み取りの同時実行を許容し、排他ロックは更新や削除などの変更を安全に行うため、他の読み取りや更新を制限します。sql for update は典型的に排他ロック(行単位)を取得するため、他セッションの更新競合や一部の読み取りを待機させる効果があります。

取得済み↓ / 取得要求→ 共有 (S) 排他 (X)
共有 (S) 許可(互換) 不可(競合)
排他 (X) 不可(競合) 不可(競合)
  • 共有ロックの目的: 読み取り整合性の確保(同一対象への他の読み取りは許可、更新はブロック)
  • 排他ロックの目的: 更新整合性の確保(同一対象への他の読み取り・更新をブロックするケースがある)
  • 代表例: sql for update は対象行に対する排他ロックを取得し、トランザクション完了まで保持します。

行ロックとテーブルロックの仕組み

ロックの粒度は性能と競合に直結します。一般に、粒度が細かいほど同時実行性は高く、粒度が粗いほど制御は単純ですが競合が増えやすくなります。

  • 行ロック(Row-level Lock)
    • 対象行のみをロック。sql for update が典型で、必要最小限の競合に抑えられる。
    • トランザクションが長引くと、該当行をめぐる更新待ちが増加し、スループットを低下させる可能性。
  • テーブルロック(Table-level Lock)
    • テーブル全体をロック。メタデータ変更や一部の大量更新で発生することがある。
    • 同時実行性は低下するが、競合制御は単純でオーバーヘッドは小さい場合がある。
  • ロックの寿命
    • 多くのRDBMSでは、ロックはトランザクション終了(COMMIT/ROLLBACK)まで保持される(二相ロックの考え方)。
  • スコープと計画
    • 検索条件やインデックスの選択により、実際にロックされる行の集合が変わる(対象の最小化が鍵)。

動作検証ケーススタディ

以下は、2つのセッション(セッションA/セッションB)で同一の行(例: id=1)を対象にしたときの一般的な挙動です。共有ロックは読み取り整合性を目的とし、排他ロックは更新整合性を目的とするため、互換性の有無が待機やブロックを生みます。sql for update は排他ロックでの検証に利用します。

共有ロック取得中にさらに共有ロックを取得する場合

想定シナリオ:

  1. セッションAが対象行に共有ロックを取得(読み取り)。
  2. セッションBも同一行に共有ロックを要求(読み取り)。

結果とポイント:

  • 共有 vs 共有は互換。セッションBは即時に読み取り可能。
  • ただし、いずれかのセッションがその後に更新を行おうとして排他ロックに昇格する場合、昇格時点で競合が発生し、待機(または失敗)となる。
  • 長時間の共有ロック保持は、後続の更新作業(排他ロック取得)を滞留させやすい点に注意。

共有ロック取得中に占有ロックを取得する場合

想定シナリオ:

  1. セッションAが対象行に共有ロックを取得(読み取り)。
  2. セッションBが同一行に対して更新を行うため、sql for update で排他ロックを要求。

結果とポイント:

  • 共有 vs 排他は非互換。セッションBは、セッションAがロックを解放するまで待機する。
  • もしセッションAも後から更新に進もうとすると、ロック昇格の競合でデッドロックに陥る可能性があるため、読み取りから更新へ進む設計は短いスコープで完結させるのが安全。
  • 更新優先の処理では、最初から必要最小限の対象行に対し sql for update で排他ロックを確保し、滞留時間を短く保つことが有効。

占有ロック取得中に共有/占有ロックを取得する場合

想定シナリオ:

  1. セッションAが対象行に対して sql for update を実行し、排他ロックを取得。
  2. セッションBが同一行に共有ロック(読み取り)を要求、または別の更新で排他ロックを要求。

結果とポイント:

  • 排他 vs 共有/排他はいずれも非互換。セッションBはセッションAのトランザクション完了まで待機する。
  • 排他ロックの保持時間が長いほど同時実行性が大きく損なわれるため、更新対象の絞り込み(where条件と索引活用)とトランザクションの短縮が重要。
  • バッチ処理やワーカー並列の設計では、ロックの粒度と保持時間を揃え、競合領域を最小化することでスループットを維持できる。

データベース別の挙動と注意点

sql+for+update

OracleにおけるFOR UPDATE

明示的行ロックの基本と関連するDMLの関係

Oracleでは、SELECT … FOR UPDATEは、選択された行に対してUPDATE/DELETEと同等の行レベルの排他ロック(TXロック)を取得します。これは、後続のUPDATE/DELETEを安全に行うための「予約」であり、他セッションによる同一行の変更や二重取得を防ぎます。sql for updateは、特にジョブキュー方式(未処理レコードのピックアップ)や、ユーザー操作による悲観的ロック運用に有効です。

  • 対象行はトランザクション終了(COMMIT/ROLLBACK)までロックが保持されます。
  • JOINを伴うSELECTでも、FOR UPDATE OF で指定した表(または列を含む表)の行がロック対象となります。
  • ロックは他のSELECT(読み取り)をブロックしませんが、同一行に対するUPDATE/DELETE/SELECT FOR UPDATEは待機または失敗します。
-- 後続の更新を前提に行ロックを確保
SELECT *
  FROM orders o
 WHERE o.status = 'NEW'
   FOR UPDATE OF o.status;

WAIT/NOWAIT/SKIP LOCKEDの挙動

Oracleは待機制御が柔軟です。要件に応じてタイムアウトやスキップを選べます。

  • WAIT n:最大n秒までロック解放を待機。n秒を超えるとエラーになります。
  • NOWAIT:待たずに即時エラー(ORA-00054)を返します。UIレスポンス重視の場面に有効。
  • SKIP LOCKED:ロック済みの行を飛ばして未ロック行のみを返し、それらにロックを取得します。安全な並列分散処理に適しています。
-- 最大10秒待機
SELECT * FROM tasks WHERE status = 'READY' FOR UPDATE WAIT 10;

-- 即時に失敗
SELECT * FROM tasks WHERE status = 'READY' FOR UPDATE NOWAIT;

-- ロック済み行を飛ばしてワーカーごとに分散
SELECT * FROM tasks WHERE status = 'READY' FOR UPDATE SKIP LOCKED;

UPDATEしない場合の注意点と終了処理

SELECT … FOR UPDATE後に更新を実施しないケースでも、ロックは保持されます。終了処理を誤るとロック競合・待機が連鎖します。

  • 更新見送り時はROLLBACKまたはCOMMITで必ずロックを解放してください。
  • アプリケーションの例外経路でも確実にトランザクションを終端できるよう、try-finally等でCOMMIT/ROLLBACKを徹底します。
  • 長時間ロックを避けるため、取得→処理→確定の短いトランザクションに分割します。
BEGIN
  -- 行ロック取得
  SELECT * INTO v_row FROM tasks WHERE id = :id FOR UPDATE NOWAIT;
  -- 何らかの条件で更新を見送る場合...
  ROLLBACK; -- or COMMIT
EXCEPTION
  WHEN OTHERS THEN
    ROLLBACK;
    RAISE;
END;

MySQL(InnoDB)のFOR UPDATEとLOCK IN SHARE MODE

SELECT/UPDATE時の排他・共有ロックの違い

InnoDBでは、sql for updateにより選択したインデックスレコードへ排他ロック(Xロック)が付与され、他トランザクションからの更新・削除を防ぎます。LOCK IN SHARE MODEは共有ロック(Sロック)で、他の共有ロックは許容しますが、同一行の更新はブロックします。

  • FOR UPDATE:Xロック(必要に応じてギャップロック/ネクストキーロック)を取得。後続のUPDATE/DELETE前提。
  • LOCK IN SHARE MODE:Sロックを取得。検証読み取りや整合性チェックで有用。
  • 分離レベルの影響:REPEATABLE READでは範囲検索にネクストキーロックが付与されやすく、READ COMMITTEDではギャップロックが抑制されます(ただし更新系は状況によりギャップロックが発生)。
-- 排他ロック(更新前提)
SELECT * FROM orders WHERE status = 'NEW' FOR UPDATE;

-- 共有ロック(検証読み取り)
SELECT * FROM products WHERE sku = 'A-001' LOCK IN SHARE MODE;

行ロックとテーブルロックの具体例

InnoDBは基本的に行ロックですが、条件や操作によりテーブル全体に近い広範囲ロック・待機が発生します。代表的な例を押さえておきましょう。

  • インデックス未使用の範囲更新
    • WHERE句に適切なインデックスがないUPDATE/SELECT … FOR UPDATEは多数行をスキャンし、多くの行にロックが波及します。
  • ネクストキーロック(REPEATABLE READ)
    • 範囲条件(BETWEEN, >, , LIKE ‘x%’})でFOR UPDATEすると、該当行+ギャップにもロックがかかり、実質的に大きな範囲が保護されます。
  • メタデータロック(MDL)
    • DDL(ALTER TABLEなど)とDMLはMDLで競合します。長時間のトランザクションがいると、ALTERが待機、逆にALTER中は新規DMLが待機します。
  • 明示的テーブルロック
    • LOCK TABLES WRITE/READを使うとテーブルレベルのロックになります(通常のアプリでは推奨されません)。
-- インデックスがない列での範囲更新は要注意
UPDATE orders
   SET status = 'PROCESSING'
 WHERE created_at < NOW() - INTERVAL 1 DAY; -- created_atに索引が必要

ロック待ちクエリの確認手順

ロック競合を素早く特定するには、情報スキーマ・パフォーマンススキーマ・診断ビューを組み合わせます。

  1. 待機関係の全体像を確認
    SELECT * FROM sys.innodb_lock_waits; -- 依存関係つきで見やすい
  2. トランザクションの長時間実行を洗い出し
    SELECT trx_id, trx_started, trx_state, trx_query
      FROM information_schema.innodb_trx
     ORDER BY trx_started;
  3. データロックの詳細(MySQL 8.0+)
    SELECT * FROM performance_schema.data_locks;
    SELECT * FROM performance_schema.data_lock_waits;
  4. スナップショットで状況把握
    SHOW ENGINE INNODB STATUS\G
    SHOW PROCESSLIST;
  5. 対処
    • 原因セッションのコミット促進、SQLチューニング(索引追加、範囲縮小)、必要に応じてKILL。

SnowflakeにおけるFOR UPDATE

利用可能なパラメータ項目

SnowflakeでもSELECT … FOR UPDATEにより、選択行に対し更新をブロックする行ロックが取得されます(読み取りはMVCCにより非ブロッキング)。主に以下のオプションが利用できます。

  • NOWAIT:ロック競合時に即時エラー。
  • SKIP LOCKED:ロック済みの行をスキップし、未ロック行のみを返してロック。

一般的なWAIT秒指定は提供されていません。複数テーブルを含むSELECTでは、更新可能な基表の一致行がロック対象となります。

制約事項と既知の制限

  • オートコミットが有効なクライアントでは、ステートメント終了時に即コミットされるためロックが保持されません。FOR UPDATEを活かすには明示的トランザクションが必要です。
  • FOR UPDATEは読み取りをブロックしません(MVCC)。ブロック対象は同一行への同時DMLに限定されます。
  • WAIT n秒のような細粒度の待機時間指定は不可です。即時性が要る場合はNOWAIT、ワーカー分散にはSKIP LOCKEDを使います。
  • 一部の派生オブジェクト(特定のビューなど)はロック対象外です。実テーブルを参照するクエリで使用してください。

使用上の注意点

  • BEGIN/COMMITでトランザクションを明示し、必要最小限の行だけを短時間ロックします。
  • ジョブキュー用途はSKIP LOCKEDが有効。ワーカーが同じ行を取り合うことを防止できます。
  • JOIN時は必要な表に限定した条件で対象行を絞り、不要なロック拡散を避けます。
-- オートコミットをオフにし、短いトランザクションで運用する
BEGIN;
SELECT * FROM tasks WHERE status = 'READY' FOR UPDATE SKIP LOCKED;
-- ... 処理 ...
UPDATE tasks SET status = 'DONE' WHERE id = :id;
COMMIT;

サンプルクエリ集

-- 基本形(ロックを確保)
BEGIN;
SELECT id, payload FROM job_queue WHERE status = 'READY' LIMIT 100 FOR UPDATE;
-- ...処理...
COMMIT;

-- NOWAIT(即時に失敗させる)
BEGIN;
SELECT * FROM accounts WHERE id = 123 FOR UPDATE NOWAIT;
-- ...更新...
COMMIT;

-- SKIP LOCKED(並列ワーカーで安全に分散)
BEGIN;
SELECT id FROM job_queue
 WHERE status = 'READY'
 ORDER BY created_at
 LIMIT 10
 FOR UPDATE SKIP LOCKED;
-- ...バッチ処理...
COMMIT;

その他プラットフォームの注意点(FileMaker SQLなど)

例を読み解く際の留意事項

FileMakerのExecuteSQLは読み取り専用で、sql for update(SELECT … FOR UPDATE)やUPDATE/DELETEの実行をサポートしません。レコードの排他制御はFileMaker独自のレコードロック(編集開始時に取得)で行われるため、RDBMSのFOR UPDATEと同じ発想をそのまま適用できません。以下の点に注意してください。

  • ExecuteSQLのサンプルでFOR UPDATEが登場しても、FileMaker内では機能しません(ODBC/JDBC経由で外部RDBMSに接続する場合は別)。
  • 悲観的ロックが必要なら、FileMaker側のスクリプト手順でレコードを編集状態にし、短い操作でコミットする設計にします。
  • 外部RDBMSに対しては、そのDBが提供するFOR UPDATE/NOWAIT/SKIP LOCKEDの仕様差を前提に実装します。

オブジェクト型フィールドの扱い(CASTなどの型変換)

プラットフォームによっては、BLOB/CLOB/JSON/OBJECTなどのオブジェクト型フィールドを含むテーブルに対して、FOR UPDATEと同時に扱う際に型変換が必要になる場合があります。取得列に巨大LOBやJSONを含めるとネットワークやメモリ負荷が高まり、ロック保持時間が延びることもあります。

  • 必要なカラムのみを選択し、重量級のオブジェクト列は除外するか、要約・CASTしてサイズを小さくする。
  • Oracle:CLOBをTO_CHARで部分取得、BLOBはDBMS_LOB.SUBSTRで必要範囲を取得。
  • MySQL:JSON_EXTRACTで必要キーのみを抽出、またはCAST(json_col AS CHAR)で軽量化。
  • Snowflake:OBJECT/VARIANTは:演算子で特定キーを取り出し、::STRINGでCAST(例:data:taskId::string)。
  • FileMaker(コンテナ):SQL/ODBCでの直接取得は困難な場合が多く、アプリ側APIや専用関数で扱うのが現実的。
-- Snowflake 例:必要キーのみ取得してロック
BEGIN;
SELECT id, (data:taskId)::string AS task_id
  FROM job_queue
 WHERE status = 'READY'
 FOR UPDATE SKIP LOCKED;
COMMIT;

代表的なクエリ例

database+lock+sql

条件付きSELECT … FOR UPDATEの基本例

まずは、条件を満たす行だけをロックしてから安全に更新する典型パターンです。キュー処理や在庫引当などで「対象行を取り出してから変更する」ワークフローに適しています。ここでは「READYなジョブを1件だけ先着順で取得し、処理中にする」ケースを例にします。sql for update を使うことで、同時実行時の取り合いを防げます。

-- PostgreSQL / MySQL 8.0 の例
START TRANSACTION;

-- 1件だけロックして取得
SELECT id, payload
FROM job_queue
WHERE status = 'READY'
ORDER BY priority DESC, created_at ASC
LIMIT 1
FOR UPDATE;

-- アプリ側で処理をはじめる前に状態を更新
UPDATE job_queue
SET status = 'RUNNING', started_at = NOW()
WHERE id = <上で取得したid>;

COMMIT;
-- Oracle の例
BEGIN
  NULL;
END;
/

-- Oracle では FETCH FIRST で 1件だけ取得
SELECT id, payload
FROM job_queue
WHERE status = 'READY'
ORDER BY priority DESC, created_at ASC
FETCH FIRST 1 ROWS ONLY
FOR UPDATE;

UPDATE job_queue
SET status = 'RUNNING', started_at = SYSTIMESTAMP
WHERE id = :id;

COMMIT;
  • ポイント: 条件式(WHERE)で対象を絞り、ORDER BY で確定的な順序を与え、最後に FOR UPDATE で行ロックを取得します。
  • 副作用防止: 取得した行だけを更新するため、主キー(id)を使ったUPDATEで対象を特定します。
  • トランザクション: SELECTからUPDATE、COMMITまでを1トランザクションで実施します。

SKIP LOCKED/NOWAITを使った同時実行制御の例

ワーカーが複数同時に走る前提なら、既に他トランザクションがロック中の行を避けて進む「SKIP LOCKED」や、待たずに即時エラーにしたい「NOWAIT」が有効です。sql for update にオプションを付けるだけで、スループットや待機戦略を調整できます。

-- PostgreSQL / MySQL 8.0: SKIP LOCKED でロック済み行を飛ばしてバッチ確保
START TRANSACTION;

SELECT id
FROM job_queue
WHERE status = 'READY'
ORDER BY priority DESC, id
LIMIT 10
FOR UPDATE SKIP LOCKED;  -- すでに他トランザクションが掴んだ行はスキップ

-- 取得できたIDだけを一括で「RUNNING」に
UPDATE job_queue
SET status = 'RUNNING', worker_id = :worker
WHERE id IN (<上で取得した複数IDのリスト>);

COMMIT;
-- Oracle: SKIP LOCKED / NOWAIT の例
-- スキップして取り続ける(取り合いを回避)
SELECT id
FROM job_queue
WHERE status = 'READY'
ORDER BY priority DESC, id
FETCH FIRST 10 ROWS ONLY
FOR UPDATE SKIP LOCKED;

-- 即時失敗させたい場合(待たずに制御をアプリ側に戻す)
SELECT id
FROM accounts
WHERE id = :id
FOR UPDATE NOWAIT;
  • SKIP LOCKED: ロック済み行を読み飛ばすため、並列ワーカーでのキュー消化に最適。待機が発生しにくくスループット向上に寄与します。
  • NOWAIT: ロック待ちをせずに即時エラーにし、アプリ側でリトライや別処理に切り替える戦略をとれます。
  • バッチ処理: LIMIT(または FETCH FIRST)と併用して一度に確保する件数を制御します。

カーソルとCURRENT OFで更新する例

行指向に処理したい場合、カーソルでロックしながら1件ずつ取り出し、対応する行を「WHERE CURRENT OF」で更新する手法が使えます。sql for update とカーソルを組み合わせると、更新対象の特定が明快になり、競合が起きづらい安全な逐次更新が可能です。

-- Oracle PL/SQL の例(FOR UPDATE OF を併用)
DECLARE
  CURSOR c_jobs IS
    SELECT id, attempts
    FROM job_queue
    WHERE status = 'READY'
    ORDER BY priority DESC, id
    FOR UPDATE OF status, attempts SKIP LOCKED;
BEGIN
  FOR r IN c_jobs LOOP
    -- 対象行はカーソルが保持するロックで保護されている
    UPDATE job_queue
      SET status   = 'RUNNING',
          attempts = r.attempts + 1,
          worker_id = :worker
    WHERE CURRENT OF c_jobs;
    -- 必要ならここで処理実行
  END LOOP;
  COMMIT;
END;
/
-- PostgreSQL の例(サーバーサイドカーソル)
BEGIN;

DECLARE c_jobs CURSOR FOR
  SELECT id
  FROM job_queue
  WHERE status = 'READY'
  ORDER BY priority DESC, id
  FOR UPDATE SKIP LOCKED;

-- 1件ずつフェッチして、その場で更新
FETCH NEXT FROM c_jobs;  -- id を受け取る
UPDATE job_queue
SET status = 'RUNNING', worker_id = :worker
WHERE CURRENT OF c_jobs;

-- 必要に応じて繰り返し
-- FETCH NEXT ... / UPDATE ... WHERE CURRENT OF c_jobs;

COMMIT;
  • 逐次処理: 行ごとにロックを保持したまま更新でき、処理の整合性を担保しやすくなります。
  • 可読性: WHERE CURRENT OF により、選択済み行との対応関係が明確です。
  • 拡張: SKIP LOCKED を併用すると、他ワーカーと取り合わずに安全な並列処理が実現できます。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です