トランザクションとACID特性:PostgreSQLにおけるデータ整合性の核
1. トランザクションとは?
トランザクションとは、一連のデータベース操作を一つの論理的な作業単位としてまとめたものです。この作業単位は、「すべて成功する」か「すべて失敗する」かのどちらかの結果しかありません。途中の状態(一部だけ成功)で終わることは決してありません。
例えば、銀行の口座振替を考えてみましょう。
- Aさんの口座から10,000円を引き出す。
- Bさんの口座に10,000円を振り込む。
この2つの操作は、必ずセットで実行される必要があります。1の操作だけが成功して、2の操作が失敗した場合、Aさんの口座から10,000円が消えてしまい、大問題になります。トランザクションは、このような一連の操作を一つのパッケージとして扱い、データの整合性を保証します。
PostgreSQLでは、以下のコマンドでトランザクションを制御します。
BEGINまたはSTART TRANSACTION: トランザクションを開始します。COMMIT: トランザクション内のすべての変更を確定し、データベースに永続的に反映します。ROLLBACK: トランザクション内のすべての変更を取り消し、トランザクション開始前の状態に戻します。
BEGIN;
-- usersテーブルからid=1のユーザーの残高を1000減らす
UPDATE accounts SET balance = balance - 1000 WHERE user_id = 1;
-- usersテーブルからid=2のユーザーの残高を1000増やす
UPDATE accounts SET balance = balance + 1000 WHERE user_id = 2;
-- ここでエラーが発生した場合や、条件が満たされない場合はROLLBACKする
-- IF some_condition THEN
-- ROLLBACK;
-- ELSE
-- COMMIT;
-- END IF;
COMMIT; -- 全ての変更を確定2. ACID特性
トランザクションが信頼できるものであるために、満たすべき4つの重要な性質があります。これらの頭文字をとってACID特性と呼ばれます。
(A) 原子性 (Atomicity)
「All or Nothing(全か無か)」 の原則です。トランザクション内のすべての操作が完全に実行されるか、あるいは一つも実行されないかのどちらかであることが保証されます。
PostgreSQLは、WAL(Write-Ahead Logging)機構を用いて原子性を保証しています。トランザクションの変更内容は、まずWALバッファに書き込まれ、ディスク上のWALファイルに永続化されます。COMMITされると、そのトランザクションが成功したことが記録されます。もし途中でシステムがクラッシュしても、PostgreSQLは再起動時にWALをチェックし、コミットされていないトランザクションの変更をすべてロールバックして、データベースを矛盾のない状態に戻します。
(C) 一貫性 (Consistency)
トランザクションの前後で、データベースの状態が一貫していることを保証します。一貫性とは、データベースに予め定められたルール(制約)を常に満たしている状態を指します。
例えば、「口座の残高はマイナスになってはならない」という制約がある場合、トランザクションによって残高がマイナスになるような操作は、たとえ計算が正しくても最終的にロールバックされ、制約が破られることはありません。
一貫性は、以下の要素によって保たれます。
- 主キー制約、外部キー制約、NOT NULL制約、CHECK制約など
- トリガー
- アプリケーションロジック
(I) 独立性 (Isolation)
複数のトランザクションが同時に実行された場合でも、それぞれのトランザクションは互いに干渉せず、独立して実行されているように見えることを保証します。これにより、他のトランザクションが中途半端に実行した結果(コミットされていないデータ)を読み込んでしまうといった問題を防ぎます。
ただし、独立性を厳密に保つと、同時に実行できる処理が少なくなり、システムのパフォーマンスが低下する可能性があります。そのため、PostgreSQLでは、アプリケーションの要件に応じて独立性のレベル(トランザクション分離レベル)を選択できます。
(D) 永続性 (Durability)
一度COMMITされたトランザクションの結果は、その後にシステム障害(クラッシュや電源断など)が発生しても失われないことを保証します。
これもWALによって実現されています。COMMITが成功した時点で、そのトランザクションの変更内容はディスク上のWALファイルに書き込まれていることが保証されます。たとえデータファイルへの反映が完了する前にシステムがクラッシュしても、再起動時にWALの記録を元に変更を再現(リカバリ)することができます。
3. トランザクション分離レベル
独立性(Isolation)のレベルは、SQL標準で4段階定義されており、PostgreSQLもこれらをサポートしています。レベルが低いほどパフォーマンスは向上しますが、同時に実行されるトランザクション間で問題が発生する可能性(競合現象)が高まります。
| 分離レベル | 発生しうる競合現象 | 説明 |
|---|---|---|
| Read Uncommitted | Dirty Read, Non-Repeatable Read, Phantom Read | 他のトランザクションがコミットしていない変更も読み込める。PostgreSQLではRead Committedと同じ動作。 |
| Read Committed | Non-Repeatable Read, Phantom Read | 他のトランザクションがコミットした変更のみ読み込める。PostgreSQLのデフォルト。 |
| Repeatable Read | Phantom Read | トランザクション開始時点のスナップショットを見るため、同じトランザクション内では何度同じ行を読んでも同じ結果が返る。 |
| Serializable | - | 複数のトランザクションが完全に独立して実行されることを保証する最も厳格なレベル。 |
競合現象
- Dirty Read(ダーティリード): 他のトランザクションがまだコミットしていない、変更中のデータを読み込んでしまう現象。
- Non-Repeatable Read(ノンリピータブルリード): 同じトランザクション内で同じ行を2回読み込んだ際に、1回目と2回目で結果が異なる現象。他のトランザクションがその行を更新しコミットした場合に発生。
- Phantom Read(ファントムリード): 同じトランザクション内で同じ条件で2回検索を行った際に、1回目にはなかった行が2回目に出現する現象。他のトランザクションが新しい行を挿入しコミットした場合に発生。
分離レベルの設定
分離レベルは、トランザクションの開始時に設定します。
-- セッション全体のデフォルト分離レベルを設定
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 現在のトランザクションの分離レベルのみを設定
BEGIN ISOLATION LEVEL SERIALIZABLE;
-- トランザクション開始後に設定することも可能
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
...
COMMIT;通常は、デフォルトのRead Committedで十分なケースが多いですが、より厳密な一貫性が求められる処理(例:複雑な集計処理、整合性が重要な金融取引など)ではRepeatable ReadやSerializableの利用を検討します。
まとめ
トランザクションとACID特性は、リレーショナルデータベースが長年にわたって信頼性を維持してきた中心的な概念です。PostgreSQLは、WALやMVCC(Multi-Version Concurrency Control)といった洗練された仕組みを用いて、これらの特性を堅牢に実装しています。
開発者は、個々のSQL文だけでなく、どこからどこまでが一つの論理的な作業単位なのかを意識し、適切にトランザクションを利用することが、堅牢で信頼性の高いアプリケーションを構築する上で非常に重要です。