SQL標準では、4つのトランザクション分離レベルが定義されています。最も厳密なのは直列化可能です。直列化可能トランザクションの同時実行は、それらをある順序で1つずつ実行した場合と同じ効果を生み出すことが保証されます。他の3つのレベルは、防止しなければならない同時実行現象によって定義されます。
同時実行現象
ダーティリード
トランザクションが、同時実行されている未コミットのトランザクションによって書き込まれたデータを読み取ります。
ノンリピータブルリード
トランザクションが、以前に読み取ったデータを再度読み取り、最初の読み取り以降にコミットされた別のトランザクションによってデータが変更されていることを検出します。
ファントム読み取り
トランザクションが、検索条件を満たす行のセットを返すクエリを再実行し、最近コミットされた別のトランザクションによって条件を満たす行のセットが変更されていることを検出します。
シリアル化アノマリー
トランザクションのグループを正常にコミットした結果が、それらのトランザクションを1つずつ実行するすべての可能な順序と一貫性がない状態です。
分離レベル
次の表は、SQL標準で定義され、PolarDB for PostgreSQL で実装されている、各分離レベルが許可または防止する現象を示しています。
| 分離レベル | ダーティリード | ノンリピータブルリード | ファントムリード | シリアル化アノマリー |
|---|---|---|---|---|
| リードアンコミッティド | 許可されますが、PGでは許可されません | 可能 | 可能 | 可能 |
| 読み取りコミット済み | 不可能 | 可能 | 可能 | 可能 |
| リピータブルリード | 不可能 | 不可能 | 許可されますが、PGでは許可されません | 可能 |
| 直列化可能 | 不可能 | 不可能 | 不可能 | 不可能 |
PolarDB for PostgreSQL は、4つの標準的な分離レベル要求をすべて受け入れますが、内部的には3つの異なるレベルのみを実装しています。標準では、必要最小限よりも高い保証が許可されているため、リピータブルリードの実装でもファントムリードが防止されます。
PolarDB for PostgreSQL では、リードアンコミッティドはリードコミッティドと同一に動作します。これは、標準の分離レベルを PostgreSQL の Multi-Version Concurrency Control (MVCC) アーキテクチャにマッピングする唯一の合理的な方法です。ご利用のアプリケーションがリードアンコミッティドを要求した場合、リードコミッティドで実行されます。
トランザクションの分離レベルを設定するには、SET TRANSACTION を使用します。
一部の PostgreSQL のデータの型と関数には、特別なトランザクション動作があります。serial で宣言された列のカウンターを含むシーケンスに加えられた変更は、他のすべてのトランザクションに即座に表示され、変更を行ったトランザクションが中止されてもロールバックされません。
リードコミッティド分離レベル
リードコミッティドは、PostgreSQL のデフォルトの分離レベルです。このレベルでは、次のようになります。
SELECT(FOR UPDATE/FOR SHAREなし) は、クエリが開始される前にコミットされたデータのみを参照します。未コミットのデータや、クエリ実行中に同時実行トランザクションによってコミットされた変更は参照しません。SELECTは、自身のトランザクションにおける以前の更新の影響を、まだコミットされていなくても参照します。同じトランザクション内の2つの連続する
SELECTコマンドは、それらの間に別のトランザクションが変更をコミットした場合、異なるデータを返す可能性があります。
UPDATE、DELETE、SELECT FOR UPDATE、および SELECT FOR SHARE は、コマンド開始時刻にコミットされたターゲット行を検索します。ターゲット行が、まだ進行中の同時実行トランザクションによって更新または削除されている場合、コマンドはそのトランザクションがコミットまたはロールバックされるまで待機します。
同時実行トランザクションがロールバックされた場合、待機中のコマンドは元の行で処理を続行します。
同時実行トランザクションがコミットし、行を削除した場合、待機中のコマンドはその行を無視します。
同時実行トランザクションがコミットし、行を更新した場合、待機中のコマンドは更新されたバージョンに対して
WHERE句を再評価し、行がまだ一致する場合は処理を続行します。
INSERT と ON CONFLICT DO UPDATE を使用すると、無関係なエラーが発生しない限り、提案された各行が挿入または更新されることが保証されます。INSERT と ON CONFLICT DO NOTHING を使用すると、INSERT スナップショットにまだ影響が見えないトランザクションから競合が発生した場合、行がスキップされることがあります。
リードコミッティドのトレードオフ
リードコミッティドでは、単一のコマンドが一貫性のないスナップショットを参照できます。つまり、処理中の行に対する同時更新は参照できますが、データベース内の他の行に対する同時更新は参照できません。このため、複雑な検索条件を持つコマンドには不向きですが、単純なケースではうまく機能します。
例: 銀行残高振替 (正しく機能する場合)
BEGIN;
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 12345;
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 7534;
COMMIT;各コマンドは所定の行のみに影響するため、更新された行バージョンを参照しても一貫性のない状態は発生しません。
例: DELETE のエッジケース (予期せぬ動作)
website テーブルに hits の値が9と10の2つの行がある場合:
BEGIN;
UPDATE website SET hits = hits + 1;
-- 別のセッションから実行: DELETE FROM website WHERE hits = 10;
COMMIT;DELETE は、UPDATE の前後で hits = 10 の行が存在していたにもかかわらず、効果がありませんでした。DELETE スナップショットによって更新前の値9はスキップされ、UPDATE がコミットされた後、行の値は11となり、WHERE hits = 10 の条件に一致しなくなります。
リードコミッティドは、多くのアプリケーションに対して十分な分離を提供し、高速に利用できます。より一貫性のあるビューを必要とする複雑なクエリや更新を伴うアプリケーションの場合は、リピータブルリードまたは直列化可能を使用してください。
リピータブルリード分離レベル
リピータブルリードは、トランザクションが開始される前にコミットされたデータのみを参照します。未コミットのデータや、トランザクション中に同時実行トランザクションによってコミットされた変更は参照しません。リードコミッティドとは異なり、スナップショットはトランザクション内の最初の非トランザクション制御ステートメントの開始時に取得され、個々のステートメントの開始時ではありません。同じトランザクション内の連続する SELECT コマンドは、常に同じデータを参照します。
このレベルは、上記の表にあるシリアル化アノマリーを除くすべての現象を防止します。これは、リピータブルリードに対する SQL標準の最小要件を超えています。
このレベルを使用するアプリケーションは、シリアル化の失敗によるトランザクションのリトライに備える必要があります。
UPDATE、DELETE、SELECT FOR UPDATE、および SELECT FOR SHARE は、トランザクション開始時刻にコミットされた行を検索します。ターゲット行が同時実行トランザクションによって更新または削除されている場合:
同時実行トランザクションがロールバックされた場合、リピータブルリードトランザクションは元の行で処理を続行します。
同時実行トランザクションがコミットし、実際にその行を更新または削除した場合 (ロックしただけではない場合)、リピータブルリードトランザクションは次のエラーでロールバックされます。
ERROR: could not serialize access due to concurrent update
このエラーが発生した場合、現在のトランザクションを中止し、最初からリトライしてください。2回目の試行では、トランザクションは以前にコミットされた変更を含むスナップショットで開始されるため、論理的な競合はありません。
更新トランザクションのみがリトライする必要があります。読み取り専用トランザクションはシリアル化の競合に遭遇することはありません。
リピータブルリードは、各トランザクションにデータベースの一貫性のあるスナップショットを提供しますが、そのスナップショットは、同時実行トランザクションのシリアル (1つずつ) 順序と必ずしも一貫性があるとは限りません。たとえば、読み取り専用トランザクションは、バッチが完了したことを示すように更新された制御レコードを参照する場合がありますが、そのバッチの論理的な一部である詳細レコードを見逃すことがあります。これは、制御レコードの以前のリビジョンを読み取ったためです。厳密なシリアル順序を必要とするビジネスルールを適用するには、明示的なロックを使用してください。
リピータブルリードは、学術的なデータベース文献で説明されている手法であるスナップショット分離を使用して実装されています。動作とパフォーマンスは、従来のロックを使用するシステムとは異なる場合があります。
直列化可能分離レベル
直列化可能は、最も厳密なトランザクション分離を提供します。これは、すべてのコミットされたトランザクションが同時ではなく、1つずつ実行されたかのように、シリアルなトランザクション実行をエミュレートします。
直列化可能は、リピータブルリードとまったく同じように機能しますが、1つ追加の機能があります。それは、同時実行される直列化可能トランザクションのセットが、可能なシリアル順序と一貫性のない結果を生成する可能性のある条件をモニターすることです。このモニタリングは、リピータブルリードがすでに必要とするブロッキングを超えるものは追加しませんが、オーバーヘッドは追加されます。潜在的なシリアル化アノマリーが検出されると、1つのトランザクションはコミットが許可され、他のトランザクションは次のエラーでロールバックされます。
ERROR: could not serialize access due to read/write dependencies among transactionsこのレベルを使用するアプリケーションは、シリアル化の失敗 (SQLSTATE '40001') によるトランザクションのリトライに備える必要があります。
例: シリアル化アノマリー
初期データが次のようになっている mytab テーブルがある場合:
class | value
-------+-------
1 | 10
1 | 20
2 | 100
2 | 200直列化可能トランザクションAは次を計算します。
SELECT SUM(value) FROM mytab WHERE class = 1;そして、結果 (30) を class = 2 の新しい行として挿入します。同時に、直列化可能トランザクションBは次を計算します。
SELECT SUM(value) FROM mytab WHERE class = 2;そして、結果 (300) を class = 1 の新しい行として挿入します。両方のトランザクションがコミットしようとします。リピータブルリードでは、両方がコミットされます。直列化可能では、一方のみがコミットされ、もう一方はシリアル化エラーでロールバックされます。これは、AとBのシリアル順序のいずれも両方の結果を生成しないためです。
述語ロックの仕組み
真の直列化可能性を保証するために、PolarDB for PostgreSQL は述語ロックを使用します。述語ロックは、書き込みが同時実行トランザクションからの以前の読み取りの結果に影響を与えたであろう時点を追跡します。これらのロックは次のとおりです。
ブロッキングを引き起こさず、デッドロックも発生させません。
pg_locksシステムビューにmodeがSIReadLockとして表示されます。メモリを管理するために、よりきめ細かいロック (タプルロック) からより粗い粒度のロック (ページロック) に統合される場合があります。
READ ONLY トランザクションは、それ以上の競合が発生しない場合、SIReadロックを早期に解放することがあります。SERIALIZABLE READ ONLY DEFERRABLE トランザクションは、データを読み取る前にシリアル化の競合が不可能であることを確立できるまでブロックします。これは、直列化可能がブロックするがリピータブルリードがブロックしない唯一のケースです。SERIALIZABLE READ ONLY DEFERRABLE トランザクション内で読み取られたデータは、読み取られるとすぐに有効であることがわかります。これは、トランザクションがデータを読み取る前に、そのような問題がないことが保証されたスナップショットを取得できるまで待機するためです。読み書きトランザクションの SIReadロックは、重複する読み書きトランザクションが完了するまで、トランザクションコミット後も保持されることがよくあります。
アノマリーを防止するために直列化可能トランザクションに依存する場合、永続的なユーザーテーブルから読み取られたデータは、それを読み取ったトランザクションが正常にコミットされるまで有効とみなしてはなりません。遅延可能な読み取り専用トランザクション以外のすべての場合において、アプリケーションは後で中止されたトランザクション中に読み取られた結果に依存してはなりません。代わりに、トランザクションが成功するまでリトライする必要があります。
同時開発の簡素化
直列化可能トランザクションを一貫して使用することで、アプリケーション開発を簡素化できます。正常にコミットされた同時実行の直列化可能トランザクションのセットが、それらが1つずつ実行された場合と同じ効果を持つという保証は、次のことを意味します。単一のトランザクションが単独で実行されたときに正しく機能する場合、直列化可能トランザクションの任意の組み合わせでも正しく機能するか、またはコミットされません。これにより、すべての可能な同時実行インタラクションについて推論する必要がなくなります。
パフォーマンスに関する考慮事項
直列化可能トランザクションで最適なパフォーマンスを得るには、次の点に注意してください。
可能な場合は、トランザクションを
READ ONLYとして宣言します。アクティブ接続の数を制御します。ビジーなシステムでは接続プールを使用します。
整合性のため、トランザクションは必要なだけ短く保ちます。
必要以上にトランザクション内で接続をアイドル状態にしないようにします。
idle_in_transaction_session_timeoutを使用して、残存するセッションを自動的に切断します。直列化可能トランザクションは同等の保護を自動的に提供するため、不要になった明示的なロック、
SELECT FOR UPDATE、およびSELECT FOR SHAREを削除します。述語ロックテーブルメモリが枯渇した場合、システムはページレベルロックをリレーションレベルロックに統合し、シリアル化失敗率が増加します。これを回避するには、
max_pred_locks_per_transaction、max_pred_locks_per_relation、および/またはmax_pred_locks_per_pageを増やしてください。シーケンシャルスキャンは常にリレーションレベルの述語ロックを必要とし、失敗率を増加させます。インデックススキャンを促進するには、
random_page_costを減らすか、および/またはcpu_tuple_costを増やしてください。ロールバックの削減とクエリ実行時間の全体的な変化とのバランスを取ります。
一意制約に関する考慮事項
直列化可能分離は、真のシリアル実行では発生しない場合に一意制約違反を引き起こす可能性があります。これは、重複する直列化可能トランザクションが両方ともキーを挿入しようとする場合に発生します。たとえ、それぞれが挿入前にキーが存在しないことを確認していたとしてもです。これを回避するには、競合する可能性のあるキーを挿入するすべての直列化可能トランザクションが、最初にキーの存在を明示的に確認するようにしてください。
直列化可能分離は、スナップショット分離にシリアル化アノマリーのチェックを追加することで構築された Serializable Snapshot Isolation (SSI) を使用して実装されています。