ここでは主に、トランザクションの基本機能について紹介します。
PostgreSQL トランザクションとは
トランザクションとは、一連のSQL操作を一つのまとまった処理として扱う機能です。
全ての操作が成功すればレコードの更新は確定されますが、一つの操作が失敗してしまった場合、全ての操作が取り消され、データの整合性を保証します。
トランザクションはBEGINで開始し、COMMITで確定、ROLLBACKでBEGIN以降の処理を取り消します。
BEGIN:トランザクションの開始
- BEGIN [TRANSACTION]:トランザクションの開始を表します。
[TRANSACTION]はオプションで、省略可能です。
トランザクション開始後、すべてのSQL操作はトランザクションの一部となり、トランザクションの終了(CMMITまたはROLLBACK)までは他のトランザクションからその操作結果が見えません。
BEGIN [ TRANSACTION ] [ ISOLATION LEVEL { READ COMMITTED | REPEATABLE READ | SERIALIZABLE } ]
[ READ WRITE | READ ONLY ]
[ DEFERRABLE | NOT DEFERRABLE ];
[ISOLATION LEVEL]:隔離レベル
- READ COMMITTED(デフォルト)
- 他のトランザクションがコミットした変更のみを読み取ります。
- 途中で他のトランザクションが変更を加えた場合、その変更が反映されます。
- REPEATABLE READ
- トランザクション開始時にスナップショットが作成され、それ以降のクエリはそのスナップショットに基づいて行われます。
- 同じクエリを複数回実行しても結果は変わりません。
- SERIALIZABLE
- トランザクションは他のトランザクションと直列的に実行されたかのように扱われ、他のトランザクションが同時に実行される影響を完全に排除します。
- 最も厳しい隔離レベルであり、競合が発生するとエラーが発生し、トランザクションがロールバックされる可能性があります。
隔離レベルに応じて発生しうる現象(データの不整合)
発生しうるデータの不整合として、以下があります。
- ダーティリード (Dirty Read)
ダーティリードは、他のトランザクションがコミットしていない変更を読み取ってしまう現象です。つまり、ロールバックした場合に、誤ったデータに基づいて処理してしまうリスクがあります。 - 反復不能読み取り (Non-repeatable Read)
反復不能読み取りは、トランザクション中に同じクエリを複数回実行したとき、別のトランザクションがデータを変更したため、異なる結果が返される現象です。例えば、最初のクエリで取得したデータが、再度クエリを実行した際に結果が異なることがあります。 - ファントムリード (Phantom Read)
ファントムリードは、トランザクション中に同じクエリを複数回実行したとき、別のトランザクションによって新しいデータが挿入された結果、行数が変わる現象です。たとえば、最初のクエリでは10行の結果が返されたのに、同じクエリを後で実行すると12行に増えている場合などがこれに該当します。
下表に各隔離レベルと、そのレベルで発生する可能性のある現象を示します。
隔離レベルが高くなるほど、トランザクション間の競合が増え、デッドロックのリスクも高まるため、パフォーマンスに影響を及ぼす可能性があります。
隔離レベル | ダーティリード | 反復不能読み取り | ファントムリード |
---|---|---|---|
READ COMMITTED | × | 〇 | 〇 |
REPEATABLE READ | × | × | 〇 |
SERIALIZABLE | × | × | × |
READ WRITE / READ ONLY
トランザクションがデータを変更できるかどうかを指定します。
- READ WRITE(デフォルト): トランザクション内でデータの読み書きが可能です。
- READ ONLY:トランザクション内では読み取り専用になります。
DEFERRABLE / NOT DEFERRABLE
このオプションは、READ ONLYトランザクションと併用されます。SERIALIZABLEで実行される読み取り専用トランザクションが、即時実行されるか、他のトランザクションのロックが解除されるまで遅延させるかを指定します。
- DEFERRABLE:他のトランザクションの影響を受けにくくするために、トランザクションを遅延させます。
- NOT DEFERRABLE(デフォルト): トランザクションは即時に実行します。
COMMIT:トランザクションの確定
トランザクション内で行われたすべての変更を確定してトランザクションを終了します。変更が他のトランザクションからも見えるようになります。
COMMIT;
ROLLBACK:トランザクションの取り消し
トランザクション内で行われたすべての変更を破棄し、データベースの状態をトランザクション開始前に戻してから終了します。
ROLLBACK;
SAVEPOINT:トランザクション内の特定のポイントに戻る
ロールバック時、SAVEPOINTで示したトランザクション内の特定のポイントに戻ることができます。SAVEPOINTを使用する事で、一部の操作だけやり直す事が可能です。
SAVEPOINT名を設定して、ROLLBACKで戻るSAVEPOINT名を指定します。
SAVEPOINT savepoint_name;
・
・
・
ROLLBACK TO SAVEPOINT savepoint_name;
LOCK:他のトランザクションからのアクセスを禁止する
複数のトランザクションが同時に同じデータにアクセスする際に、データの整合性や一貫性を保つために使用されます。
LOCKの種類
- レコードレベルのロック
データベース内の特定の行に対してかかるロックです。このロックは、トランザクションが特定のレコードに対して行う操作(例えばUPDATEやDELETEなど)に基づいて動作します。レコードレベルのロックは、データの粒度が小さいため、他のトランザクションへの影響を最小限に抑えることができ、並行処理の効率が高まります。
- 共有ロック (SHARE LOCK)
- 複数のトランザクションが同時に同じ行を読み取ることができるが、書き込みはできない。
- SELECT文などでかかる。
- 排他ロック (EXCLUSIVE LOCK)
- そのレコードに対する他のトランザクションからの読み取りや書き込みが禁止される。
- UPDATEやDELETEを行う際に使用されます。
- SELECT FOR UPDATE
- この形式のSELECTは、読み取った行に排他ロックをかけるため、他のトランザクションがその行を変更することを防ぎます。
- この形式のSELECTは、読み取った行に排他ロックをかけるため、他のトランザクションがその行を変更することを防ぎます。
- 共有ロック (SHARE LOCK)
下記の例は、account_id = 1の行に排他ロックをかけ、そのレコードを他のトランザクションが更新したり削除したりすることを防ぎます。
BEGIN;
SELECT * FROM accounts WHERE account_id = 1 FOR UPDATE;
- テーブルレベルのロック
テーブル全体に対するロックです。テーブルに対する大規模な操作や構造の変更を行う場合に使用されます。行レベルのロックよりも影響範囲が大きく、複数のトランザクションが同時にテーブルにアクセスする際の制約が大きくなります。
- ACCESS SHARE
他のトランザクションがそのテーブルを読み取ることを許可しますが、書き込みはできません。SELECT文でかかるロックです。 - ROW SHARE
このロックは、他のトランザクションがそのテーブルを読み取ることを許可しますが、テーブル全体に対する書き込みや構造変更を行うことを防ぎます。つまり、テーブルレベルでの大きな変更(ALTER TABLEやVACUUM FULLなど)が制限されますが、行レベルの更新(UPDATEやDELETE)は許可されます。 - EXCLUSIVE
他のトランザクションがそのテーブルを読み取ったり書き込んだりすることを防ぎます。これは通常、大規模な変更やデータ移行時に使われます。 - ACCESS EXCLUSIVE
他のすべてのトランザクションがそのテーブルを読み取ったり書き込んだりすることを禁止します。DROP TABLEやALTER TABLEなど、テーブル全体の操作を行う場合に適用されます。
- ACCESS SHARE
デッドロック
ロックを使用すると、2つのトランザクションが互いに相手のロックを待ち続ける状況、つまりデッドロックが発生することがあります。PostgreSQLではデッドロックを検出し、自動的に一方のトランザクションを強制的にロールバックすることでデッドロックを解消します。
現在のロック状況を確認するには、PostgreSQLのpg_locksシステムビューを使用します。
SELECT * FROM pg_locks;
トランザクションID(TXID)の枯渇問題
PostgreSQLでは各トランザクションにユニークなトランザクションID (TXID) が付与されます。しかし32ビットで管理されているため、TXIDが大量に消費されると「トランザクションIDの枯渇」が発生します。これを回避するために、定期的にVACUUMを実行し、古い不要なTXIDを回収する必要があります。
- 解決策:
- 定期的なVACUUMプロセスの実行。
- 自動バキュームの設定を適切に調整して、TXIDの管理を効率化する。
- 基本的なVACUUMの実行
データベース内のすべてのテーブルに対してVACUUMを実行します。しかし、不要なデータ(削除された行など)は削除されますが、インデックスの再構築は行われません。
VACUUM;
- 特定のテーブルに対する実行
my_tableテーブルに対してのみVACUUMを実行します。
VACUUM my_table;
- VACUUM FULL
テーブル内のデータを完全に再整理し、未使用のスペースを物理的に開放します。実行中、テーブルはロックされ、他の操作ができなくなるので、慎重に使用する必要があります。
VACUUM FULL my_table;
- VACUUM ANALYZE
VACUUMを実行した後に、テーブルの統計情報を更新し、クエリプランナーが効率的なクエリ計画を立てられるようにします。
VACUUM ANALYZE my_table;
- VERBOSEオプション
VERBOSEオプションを付けると、VACUUMの詳細な進行状況が表示されます。
VACUUM VERBOSE my_table;
まとめ
- トランザクションは「BEGIN」で開始する。
- 隔離レベルを使用する事で、データの整合性を保証する。
- 隔離レベルに応じて発生しうるデータ不整合の現象は、以下の3種類である。
- ダーティリード (Dirty Read)
- 反復不能読み取り (Non-repeatable Read)
- ファントムリード (Phantom Read)
- COMMITでトランザクションを確定する。
- ROLLBACKで、トランザクションの変更を取り消す。
- ROLLBACK時、SAVEPOINTを指定するとSAVEPOINTまでの処理が取り消される。
- LOCKにはレコードに対するLOCKとテーブルに対するLOCKがある
- トランザクションID(TXID)の枯渇に対しては、定期的びVACUUMプロセスを実行する。