【SQL】SELECT FOR UPDATEの使い方と注意点

この記事では、SQLでの並行処理で重要となる「SELECT FOR UPDATE」の基本と注意点を分かりやすく解説します。

アプリケーションには複数のユーザーが同時にアクセスするため、同一データに対して競合が発生する可能性があります。

このような競合を防ぎ、データの整合性を守るために役立つのが「SELECT FOR UPDATE」です。

私自身も実務で初めて触れた際は仕組みを理解するのに苦労したため、同じ悩みを持つ方に向けて分かりやすく整理しました。

この記事を読むメリット

  • SELECT FOR UPDATEの基本の使い方が分かる
  • 同時アクセスによるリスクがどんなものか分かる

データへの同時アクセスのリスク

まずは、同時アクセスがどのような問題を引き起こすのか整理します。

「在庫管理」を行っているアプリを例に説明します。

商品テーブル 「items」に、在庫数「stock」を管理しているとします。
idが1の商品を購入する際、次のような処理が走ります。

-- 残り在庫の確認
SELECT stock FROM items WHERE id = 1;

-- 在庫がある場合、減算して更新
UPDATE items SET stock = stock - 1 WHERE id = 1;

この処理では残りの在庫を確認して、在庫があったら更新の処理をするようにしています。

しかし、これが2人同時に行なっていたらどうでしょう。

  1. idが1の商品のstockは「1」で在庫が残り1の状態
  2. ユーザーAとユーザーBが同時に「select stock」を実行 → 残り在庫は「1」
  3. 両者とも「買える」と判断
  4. 「UPDATE」の更新処理が2回実行され、stockは「-1」となる

このようにstockがマイナスの数値になって不具合が起きてしまいます。

このようなことは、複数ユーザーを持つアプリケーションでは頻繁に発生する同時アクセスの事象です。

チケット購入や残高更新なども当てはまります。

同時アクセスにはこのようなリスクが潜んでいます。

アプリケーションの規模が大きくなるほど、このようなリスクも高まります。

SELECT FOR UPDATEによる解決方法

ここで登場するのが「SELECT FOR UPDATE」です。

先ほどのような同時アクセスによる競合を防いでくれます。

この構文を使うことで、対象の行に「排他ロック書き込みロック)」をかけることができます。

先ほどの在庫管理の例を修正するとこのようになります。

BEGIN;

-- 在庫をロックしながら取得
SELECT stock FROM items WHERE id = 1 FOR UPDATE;

-- 在庫があれば減らす
UPDATE items SET stock = stock - 1 WHERE id = 1;

COMMIT;

「SELECT stock FROM items WHERE id = 1 FOR UPDATE;」の部分で対象の行に対してロックがかかり、他のトランザクションがその行にアクセスしてロックを取得しようとすると、「ロックが解除されるまで待たされる」ようになります。

これにより、同時にデータが更新されることを防げます。

他のトランザクションは、ロック中のトランザクションの「COMMIT」が完了した時点で実行されます。

SELECT FOR UPDATEの注意点

SELECT FOR UPDATEを使用する際には注意点もあります。

1つ目は「トランザクション内で使用する」ということです。

BEGIN;

SELECT stock FROM items WHERE id = 1 FOR UPDATE;

-- 更新処理

COMMIT;

このように「BEGIN」から「COMMIT」の中で使用しないと「FOR UPDATE」を使ってもロックがかかりません。

必ずトランザクション内で使用するようにしましょう。

2つ目は「不要なロックはしないようにする」です。

SELECT FOR UPDATEで排他ロックをしている間は他のユーザーの処理は止まってしまいます。

読み取り専用の処理にまでロックをかけると、アプリ全体のパフォーマンスが低下します。あくまで同時更新の危険性がある場面に限定して使うことが重要です。

楽観的ロックというものもある

先ほどまで紹介してきた「SELECT FOR UPDATE」ですが、これは「悲観的ロック」と言われます。

より厳密にデータの整合性をとるロック方法です。

一方で、「楽観的ロック」という方法もあります。

以下が楽観的ロックの簡単な例です。

-- 現在のデータ(version=3)を取得
SELECT stock, version FROM items WHERE id = 1;

-- 更新時にversionを条件に含める
UPDATE items
SET stock = stock - 1,
    version = version + 1
WHERE id = 1 AND version = 3;

バージョンを使った方法になります。

WHEREでversionをチェックしているので、もし他の処理でversionが更新されていた場合、このUPDATEは失敗します。

失敗した際の再試行処理やエラー処理などを書かないといけないですが、こういうロックの方法もあります。

悲観的ロックとの違いをまとめると、以下のようになります。

項目 悲観ロック 楽観ロック
データ競合の頻度 高い 低い
パフォーマンス 低くなりやすい 高くなりやすい
実装の複雑さ シンプル 再試行処理が必要
安全性 高い 競合時失敗の考慮必要
ユースケース 在庫、残高、予約 プロフィール、設定

それぞれの違いを理解し、使い分ける必要があります。

まとめ

いかがだったでしょうか。

アプリケーション規模が大きくなるほど、データ更新の競合対策はより重要になります。

初めて見る内容は積極的に調べ、自分のものになるまで理解する姿勢を大切にしたいと思います。

初めてSELECT FOR UPDATEを見た方の理解の助けになれば幸いです。

コメント

この記事へのコメントはありません。

5つの質問で自社に合った進め方がわかる 業務診断をはじめる 無料・登録不要 全5問 約3分 5つの質問で自社に合った進め方がわかる 業務診断をはじめる 無料・登録不要 全5問 約3分