PostgreSQLを使ってアプリケーションを開発していると、複数のユーザーが同時に同じデータを扱う場面に出くわすことがあります。
例えば、在庫管理システムや予約システムなどでは、「同時にアクセスされたらどうなるの?」という問題が付きまといますよね。
そこで登場するのが「行ロック」です。
行ロックを使えば、特定の行に対して他のトランザクションからの変更を一時的にブロックし、データの整合性を保つことができます。
この記事では、PostgreSQLで行をかける方法について、基本から具体例までわかりやすく解説していきます。
PostgreSQLにおける行ロックの基礎知識
PostgreSQLでは、テーブル内の特定の行に対してロックをかけることができます。これを「行ロック」と呼びます。行ロックは、同時に複数のユーザーが同じデータを更新することで発生する競合や矛盾を防ぐために重要な仕組みです。
例えば、同じ商品に対して2人のユーザーが同時に在庫を減らそうとした場合、行ロックを使わなければ、在庫が意図せずマイナスになるといった問題が起こりかねません。
PostgreSQLには、暗黙的なロックと明示的なロックの2種類があります。
- 暗黙的ロックは、
UPDATE
やDELETE
といった操作を実行したとき、自動的に対象の行に対して排他ロックがかかるものです。 - 明示的ロックは、
SELECT ... FOR UPDATE
(後述で紹介)のような読み取り時点で自分でロックをかける方法です。
特に重要なのが、行ロックはトランザクションとセットで使うという点です。トランザクションを使わないと、ロックの効果が期待どおりに働かないことがあるため注意が必要です。
SELECT … FOR UPDATE の使い方(排他的ロック)
PostgreSQLで明示的に行ロックをかけたい場合、最もよく使われるのが SELECT ... FOR UPDATE
です。この構文を使うことで、読み取った行に排他ロック(他のトランザクションが更新できないロック)をかけることができます。
基本構文と使い方
まずは基本的な使い方を見てみましょう。
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 必要な処理をここで行う(例:値の確認や更新など)
COMMIT;
このクエリでは、users
テーブルの中から id = 1
の行を選択し、その行に対して排他ロックをかけるという意味になります。トランザクションが終了(COMMIT
または ROLLBACK
)するまで、この行に対して他のトランザクションが UPDATE
や DELETE
を実行しようとすると、処理がブロックされて待たされます。
どんな時に使うのか?
典型的なユースケースとしては以下のような場面があります。
- 在庫数の確認 → 減算して更新
- ユーザーのステータス確認 → 状態を変更
- 予約状況の確認 → 空き枠の確保
こうした「一度データを読み取り、それに応じて何らかの更新を行う」ような処理では、事前にロックをかけることで同時実行による不整合を防ぐことができます。
他のトランザクションからはどう見える?
他のトランザクションが同じ行にアクセスしようとすると、ロックが解放されるまで待たされます。
たとえば、2つのセッションが同時に以下のクエリを実行しようとした場合、
-- セッション1
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- ロック中...
-- セッション2(同時)
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- ← セッション1のCOMMITまで待機!
このように、FOR UPDATE
を使うと、競合を防ぐためにロック待ちが発生することがあります。これにより、同時実行時の不整合やデータ破壊を防げるというわけです。
SELECT … FOR SHARE の使い方(共有ロック)
SELECT ... FOR UPDATE
は対象の行を排他的にロックするのに対し、SELECT ... FOR SHARE
は共有ロックをかけるための構文です。これは、他のトランザクションと「読み取り」を共有したいときに使います。
基本構文と意味
BEGIN;
SELECT * FROM users WHERE id = 1 FOR SHARE;
-- 読み取った情報を元に処理を続ける(ただし更新は不可)
COMMIT;
このクエリでは、id = 1
の行に共有ロックがかかります。共有ロック中は、他のトランザクションが同じ行に対して共有ロックをかけることが可能ですが、排他ロック(FOR UPDATE
)や実際の更新処理(UPDATE
, DELETE
)はブロックされます。
どんな時に使うのか?
共有ロックは、次のような「確認系」の処理に向いています。
- 外部キー制約がある行の存在チェック
- ある行が他のトランザクションで削除されないように保護したい場合
- 読み取り専用の処理だが、他からの更新は一時的に防ぎたい場合
たとえば、ある商品が存在していることを確認してから別のテーブルにレコードを追加する場合、その商品が削除されてしまうのを防ぐために共有ロックをかけることがあります。
FOR SHARE と FOR UPDATE の違いまとめ
比較項目 | FOR UPDATE | FOR SHARE |
---|---|---|
ロックの種類 | 排他ロック(他トランザクションの更新を防ぐ) | 共有ロック(他の共有ロックはOK) |
更新可能か | 可能 | 不可 |
用途 | 読み取り後に更新する処理 | 読み取り専用・存在確認など |
このように、SELECT ... FOR SHARE
は読み取り主体のトランザクションで役立ちます。適切に使い分けることで、無駄なロック競合を避けつつ、データの整合性を保つことができます。
注意点とベストプラクティス
PostgreSQLの行ロックは便利な一方で、使い方を間違えるとパフォーマンス低下やデッドロックといったトラブルの原因になります。ここでは、行ロックを安全かつ効率よく使うためのポイントを紹介します。
トランザクションは短く保つ
ロックはトランザクションが完了するまで保持されます。そのため、ロックを取得したまま長時間処理を行うのは避けましょう。特にUIから操作されるシステムでは、ユーザーが放置したままになると他の処理がすべてブロックされる恐れがあります。
ロックの順序を統一する
複数のテーブルや複数の行をロックする処理では、ロック取得の順番をコード全体で統一することが重要です。順番が異なると、トランザクション同士が互いに相手のロックを待つ「デッドロック」が発生するリスクがあります。
ロックが不要なケースでは使わない
すべての読み取り処理に対して FOR UPDATE
を使ってしまうと、不要なロック競合が起こり、全体のパフォーマンスが低下します。
本当にロックが必要な処理かどうか、事前に要件を整理しておくことが大切です。
まとめ
この記事では、PostgreSQLにおける行ロックの基本から実践的な使い方までを解説してきました。
PostgreSQLのロック機構を正しく理解して活用することで、同時アクセスの多いシステムでも信頼性の高い処理が実現できます。
「どこでロックをかけるべきか」「どの種類のロックを使うべきか」を意識しながら設計・実装を行ってみてください。
コメント