変更要約: 初版(主題S3・S3.1〜S3.3)
3.4トランザクションの概念(分離レベル・ロック)
複数のSQL文を1つの作業単位としてまとめるトランザクション(BEGIN・COMMIT・ROLLBACK)、同時実行時の見え方を制御する分離レベル(READ COMMITTED・REPEATABLE READ・SERIALIZABLE)、明示的に排他制御を行うLOCK文、行ロックとテーブルロックの違い、複数トランザクションが互いを待ち続けるデッドロックを学びます。
銀行口座間の送金のように、複数のSQL文が「全部成功するか、全部失敗するか」でなければ整合性が壊れる場面があります。トランザクションはこの「全か無か」を保証する仕組みで、さらに複数のトランザクションが同時に実行されたときに互いへどう影響し合うかを制御する分離レベル、データの一貫性を守るロックの仕組みとあわせて理解する必要があります。
3.4.1BEGIN/COMMIT/ROLLBACKと分離レベル
- BEGIN=トランザクションを開始(
START TRANSACTIONも同義)。COMMIT=ここまでの変更を確定して永続化。ROLLBACK=ここまでの変更をすべて取り消す。PostgreSQLでは明示的にBEGINしない各SQL文も暗黙の1文トランザクションとして扱われる。 - READ COMMITTED=PostgreSQLの既定分離レベル。他トランザクションがコミット済みのデータのみ見える(コミット前の変更は見えない)が、同一トランザクション内でも問い合わせのたびに最新のコミット済みデータを見るため、同じクエリを2回実行すると結果が変わりうる(non-repeatable read)。
- REPEATABLE READ=トランザクション開始時点のスナップショットを維持し、同一トランザクション内で同じクエリを何度実行しても同じ結果が見える(他トランザクションのコミットの影響を受けない)。SERIALIZABLE=最も厳格な分離レベルで、複数トランザクションをあたかも1つずつ順番に実行したかのような整合性を保証する(実装上はシリアライズ化異常検出でロールバックさせる場合がある)。
- 分離レベルが対処する3つの読み取り異常=ダーティリード(他トランザクションの未コミットの変更を読む)/反復不能読み取り(同じ行を再読取りすると値が変わる)/ファントムリード(条件に一致する行の件数(集合)が増減する)。強い分離レベルほど防げる異常が増える。なおPostgreSQLのREPEATABLE READはスナップショット方式のため、SQL標準では同レベルで許容されるファントムリードも防止する点が特徴(=反復不能読み取りとファントムの両方を防ぐ)。
- PostgreSQLではREAD UNCOMMITTEDを指定しても内部的にはREAD COMMITTEDと同じ動作になる(他のDBMSのようにコミット前データが見えるわけではない)。この仕様の違いは試験でも狙われやすい。
3.4.2LOCK文とデッドロック
- LOCK文=テーブル全体に対して明示的にロックモードを指定する構文(
LOCK TABLE t IN ACCESS EXCLUSIVE MODE等)。通常のSQL文(UPDATE/DELETE等)は自動的に必要なロックを取得するため、明示LOCKは特殊なケースでのみ使う。 - 行ロック=更新対象の行単位でかけるロック(UPDATE/DELETEが対象行に自動的に取得。他の行への影響は無い=並行性が高い)。テーブルロック=テーブル全体にかけるロック(DDL操作等で必要になり、粒度が粗い分、並行性は下がる)。
- デッドロック=複数のトランザクションが互いに相手の保持するロックの解放を待ち続け、永久に進行できなくなる状態(例:TxAがロックXを保持しYを待ち、TxBがYを保持しXを待つ)。PostgreSQLはデッドロックを自動検出し、一方のトランザクションを強制的にエラーにしてロールバックさせることで解消する。
「PostgreSQLの既定分離レベルはREAD COMMITTED」「READ UNCOMMITTEDを指定してもREAD COMMITTEDと同じ動作」「REPEATABLE READはトランザクション開始時点のスナップショットを維持」「SERIALIZABLEが最も厳格」「行ロックは並行性が高く・テーブルロックは粗い」「デッドロックはPostgreSQLが自動検出し片方をロールバック」が最頻出です。分離レベルの厳格さの順序(READ COMMITTED < REPEATABLE READ < SERIALIZABLE)を問う設問も定番です。
在庫管理システムで「在庫を確認してから注文を確定する」処理を例に、分離レベルの違いが実際の挙動にどう影響するかを追ってみましょう。既定のREAD COMMITTEDで運用している場合、トランザクションA内でSELECT stock FROM products WHERE id=1を実行して在庫が10個であることを確認した直後、別のトランザクションBがコミットで在庫を5個に更新すると、トランザクションA内で同じSELECTをもう一度実行すれば5個という新しい値が見えることがあります(これがnon-repeatable read)。在庫確認から注文確定までの間、常に一貫した値を見たい場合はREPEATABLE READに切り替えます。BEGIN ISOLATION LEVEL REPEATABLE READ; SELECT stock FROM products WHERE id=1;のようにトランザクション開始時点のスナップショットを固定すれば、その後に他のトランザクションが何をコミットしようとも、同一トランザクション内では最初に見た10個という値のままになります。さらに厳格な整合性(複数の同時トランザクションを完全に順序立てて実行したかのように扱う)が必要な会計処理等ではSERIALIZABLEを使いますが、その代償としてシリアライズ化に失敗した際はエラーでロールバックされるため、アプリケーション側にリトライ処理を実装しておく必要があります。並行制御の観点では、在庫を更新するUPDATE products SET stock = stock - 1 WHERE id = 1は対象行にのみ行ロックをかけるため、id=2の在庫更新とは並行して進められますが、テーブル構造を変えるALTER TABLE products ADD COLUMN ...のようなDDLはテーブルロックを要求し、その間は他のトランザクションの読み書きがブロックされます。2つのトランザクションが「TxAがid=1をロックしてid=2の更新を待ち、TxBがid=2をロックしてid=1の更新を待つ」というデッドロックの状態に陥ると、PostgreSQLはこれを検出し、どちらか一方を強制的にエラーで中断させることで全体の停止を防ぎます。
| 分離レベル | 見える範囲 | 備考 |
|---|---|---|
| READ COMMITTED(既定) | クエリごとに最新のコミット済みデータ | 同一Tx内でも結果が変わりうる |
| REPEATABLE READ | トランザクション開始時点のスナップショット | 同一Tx内は常に同じ結果 |
| SERIALIZABLE | 完全に順序立てたかのような整合性 | 最も厳格・失敗時はロールバック |
ひっかけ: 「PostgreSQLでREAD UNCOMMITTEDを指定すれば、他トランザクションの未コミットの変更が見える」は誤りです。PostgreSQLはREAD UNCOMMITTEDを指定してもREAD COMMITTEDと全く同じ動作になり、コミット前のデータが見えることはありません。また「UPDATE文を発行するとテーブル全体がロックされ、他の行の更新もブロックされる」も誤り=通常のUPDATE/DELETEは対象行にのみ行ロックをかけるため、別の行への更新は並行して進められます(テーブルロックが必要なのはDDL等の特殊な操作)。
3.4.3この節のまとめ
- BEGIN→COMMIT(確定)/ROLLBACK(取消)。既定はREAD COMMITTED、REPEATABLE READはスナップショット固定、SERIALIZABLEが最も厳格
- PostgreSQLのREAD UNCOMMITTEDはREAD COMMITTEDと同じ動作という仕様上の特徴に注意
- 行ロック=対象行のみ(並行性高)・テーブルロック=全体(並行性低)。デッドロックはPostgreSQLが自動検出し片方をロールバック
進捗の記録にはログインが必要です。
理解度チェック
(軽い確認用)Q1. PostgreSQLでトランザクションの分離レベルとしてREAD UNCOMMITTEDを明示的に指定した。この場合の実際の動作について正しい説明はどれか。
Q2. あるトランザクション内で在庫数を確認してから注文処理を完了するまで、他のトランザクションのコミットに影響されず常に同じ在庫数を見たい。適切な分離レベルはどれか。
Q3. トランザクションAがid=1の行をロックしてid=2の更新完了を待ち、同時にトランザクションBがid=2の行をロックしてid=1の更新完了を待っている状態に陥った。PostgreSQLの挙動として正しいものはどれか。

