SQL の SELECT FOR UPDATE の内部動作

[![表紙](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/84429d9e-7f9b-412c-93af-09a73a045bbc.png)](https://leapcell.io/?lc_t=q_sqlselect4update)

`SELECT FOR UPDATE` は SQL における行ロックのメカニズムの一種であり、トランザクション内で取得したデータ行にロックをかけるために使用されます。

その目的は、他のトランザクションがこれらの行を変更したりロックを取得したりするのを防ぐことであり、通常はデータの一貫性を保証する必要がある場面で使用されます。

MySQL において、`SELECT FOR UPDATE` の具体的な実装はストレージエンジン（たとえば InnoDB）と密接に関係しており、その中核は行ロック（Row Lock）を利用して対象の行をロックすることにあります。

## コア機能

### ロックの種類：

- `SELECT FOR UPDATE` は、クエリで取得した行に排他ロック（Exclusive Lock、X ロック）をかけます。
- 他のトランザクションは、これらの行を変更することも、共有ロックや排他ロックを取得することもできません。

### 適用範囲：

- トランザクション内（つまり `BEGIN` と `COMMIT` の間）で使用する必要があります。
- トランザクションをサポートするストレージエンジン（たとえば InnoDB）を使用している場合にのみ有効です。

### 動作：

- もし対象の行がすでに他のトランザクションによってロックされている場合、現在のトランザクションはそのロックが解放されるかタイムアウトになるまで待機状態になります。

## 実装原理

### InnoDB の行ロックメカニズム

- InnoDB ストレージエンジンはインデックスを利用して行単位のロックを実現しています。
- `SELECT FOR UPDATE` は、クエリ条件に一致するすべての行に対してロックをかけます。クエリがインデックスを使用しない場合、テーブルロックに退化し、テーブル全体がロックされます。

### ロックのプロセス

- クエリが実行される際、InnoDB はスキャンされた各行に対して排他ロックをかけようとします。
- 特定の行がすでに他のトランザクションによってロックされている場合、現在のトランザクションはそのロックが解放されるまで待機します。

### ロックの種類

- **行ロック（Row Lock）**：インデックスに基づくロックで、クエリで取得された行のみにロックをかけます。
- **ギャップロック（Gap Lock）**：リピータブルリード（REPEATABLE READ）の分離レベルでは、範囲クエリが行を取得できなかった場合でも、範囲のギャップにロックをかけ、新しいデータの挿入を防ぎます。

### トランザクション分離レベルの影響

- **読込確定（READ COMMITTED）**：クエリごとに最新のデータを読み取り、現在のクエリ結果のみにロックをかけます。
- **リピータブルリード（REPEATABLE READ）**：トランザクションスナップショットに基づいてクエリ結果を取得し、取得できなかった行を含む範囲にもロックがかかる可能性があります（ギャップロックを使用）。
- **直列化（SERIALIZABLE）**：クエリ対象範囲全体のデータにロックがかかります。

## 実行の流れ

以下は InnoDB を例にした `SELECT FOR UPDATE` の実行フローです：

- **トランザクション開始**：`BEGIN` でトランザクションを開始します。
- **クエリとロック**：`SELECT ... FOR UPDATE` を実行し、条件を満たすレコードに排他ロック（X ロック）をかけます。
- **データ操作**：ロックされたデータを読み取ったり更新したりします。
- **ロックの解放**：`COMMIT` によってロックが解放されます。`ROLLBACK` の場合も同様にロックは解放されます。

## 例

### 基本的な使い方

```SQL
BEGIN;
SELECT * FROM orders WHERE order_id = 1 FOR UPDATE;
/* order_id=1 の行にロックをかける */
UPDATE orders SET status = 'processed' WHERE order_id = 1;
COMMIT;
```

### 複数トランザクションの競合シナリオ

**トランザクション A：**

```SQL
BEGIN;
SELECT * FROM orders WHERE order_id = 1 FOR UPDATE;
-- order_id=1 の行にロックをかける
```

**トランザクション B（トランザクション A がコミットする前）：**

```SQL
BEGIN;
SELECT * FROM orders WHERE order_id = 1 FOR UPDATE;
-- トランザクション A がロックを解放するのを待つ
```

トランザクション A がコミットまたはロールバックを行うまで、トランザクション B はロックを取得できません。

## ギャップロックの挙動

REPEATABLE READ の分離レベルでは、範囲クエリによってギャップロックが発生する可能性があります。たとえば：

```SQL
SELECT * FROM orders WHERE order_id BETWEEN 10 AND 20 FOR UPDATE;
```

このクエリの結果が空であっても、InnoDB は 10 から 20 の範囲にギャップロックをかけ、他のトランザクションがこの範囲に新しいレコードを挿入するのを防ぎます。

## 最適化と注意点

### インデックスの利用：

- インデックスを使用するクエリでは行ロックがかかり、テーブルロックへの退化を防ぐことができます。
- インデックスを使用しない場合、テーブル全体がロックされる可能性があり、同時実行性能に悪影響を及ぼします。

### 長時間トランザクションの回避：

- ロックの保持時間が長すぎると、他のトランザクションがブロックされるため、トランザクションの実行時間は可能な限り短くするべきです。

### デッドロックの検出：

- InnoDB はデッドロックを自動的に検出し、いずれかのトランザクションをロールバックさせます。デッドロックを避けるためには、トランザクションの設計を慎重に行うことが推奨されます。

### 業務要件に応じた使用：

- データの変更衝突を確実に防ぐ必要がある場合にのみ `SELECT FOR UPDATE` を使用し、不必要なロック競合を避けるようにします。

## まとめ

- `SELECT FOR UPDATE` は、MySQL において行ロックのメカニズムを通じて実現されるロック操作であり、主に並行更新の競合を防ぐために使用されます。
- その実装はトランザクション、分離レベル、インデックスの最適化に依存します。
- 適切に `SELECT FOR UPDATE` を使用すれば、データの一貫性を効果的に保護できますが、パフォーマンスコスト、ロック競合、デッドロックの問題に注意する必要があります。

---

### [私たちはLeapcell、バックエンド・プロジェクトのホスティングの最適解です。](https://leapcell.io/?lc_t=q_sqlselect4update)

[![Leapcell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/66297d38-20e3-4e98-ae60-7abbacd188b2.png)](https://leapcell.io/?lc_t=q_sqlselect4update)

[Leapcell](https://leapcell.io/?lc_t=q_sqlselect4update)は、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです：

**複数言語サポート**

- Node.js、Python、Go、Rustで開発できます。

**無制限のプロジェクトデプロイ**

- 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

**比類のないコスト効率**

- 使用量に応じた支払い、アイドル時間は課金されません。
- 例: $25で6.94Mリクエスト、平均応答時間60ms。

**洗練された開発者体験**

- 直感的なUIで簡単に設定できます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

**簡単なスケーラビリティと高パフォーマンス**

- 高い同時実行性を容易に処理するためのオートスケーリング。
- ゼロ運用オーバーヘッド — 構築に集中できます。

[ドキュメントで詳細を確認！](https://docs.leapcell.io/)

[![Try Leapcell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/cf0bc531-8e45-48b0-a6c3-70f0b6274a8a.png)](https://leapcell.io/?lc_t=q_sqlselect4update)

Xでフォローする：[@LeapcellHQ](https://x.com/LeapcellHQ)

---

[ブログでこの記事を読む](https://leapcell.io/blog/locking-rows-in-sql-inside-select-for-update)