標準SQLでは、同時に実行されるトランザクション間で防止されるべき3つの現象に対してトランザクションの隔離レベルを4レベルに分けて定義しています。 3つの望ましくない現象とは下記のものです。
4つの隔離レベルとその動作を表12-1に示します。
表 12-1. SQLトランザクション隔離レベル
隔離レベル | ダーティリード | 反復不能読み取り | ファントムリード |
---|---|---|---|
Read uncommitted | 可能性あり | 可能性あり | 可能性あり |
リードコミッティド | 安全 | 可能性あり | 可能性あり |
リピータブルリード | 安全 | 安全 | 可能性あり |
シリアライザブル | 安全 | 安全 | 安全 |
PostgreSQLはリードコミッティドとシリアライザブルの2つの隔離レベルを備えています。
PostgreSQLでは、リードコミッティドがデフォルトの隔離レベルです。 トランザクションがこの隔離レベルで実行されると、SELECT問い合わせはその問い合わせが実行される直前までにコミットされたデータのみを参照し、未だコミットされていないデータや、その問い合わせの実行中に別の同時実行トランザクションがコミットした更新は参照しません。 (とは言っても、SELECT 文は、自分自身のトランザクション内で実行され更新された結果はたとえまだコミットされていなくても参照します。) 結果として、SELECT問い合わせはその問い合わせが実行を開始した時点のデータベースのスナップショットを参照することになります。 単一のトランザクション内であっても、SELECT文を2回連続して発行した場合、最初のSELECT文を処理している最中に他のトランザクションが更新をコミットすると、最初とその次に発行したSELECT問い合わせは異なるデータを参照してしまうことを知っておいて下さい。
UPDATE、DELETE、およびSELECT FOR UPDATEコマンドは対象行を検索する際にSELECTコマンドと同じように振る舞います。 これらのコマンドは、問い合わせが開始された時点で既にコミットされている対象行のみを検出します。 しかし、その対象行は、検出されるまでに、同時実行中の他のトランザクションによって、すでに更新 (もしくは削除、もしくは更新対象としてマーク)されてしまっているかもしれません。 このような場合更新されるべき処理は、最初の更新トランザクションが(それがまだ進行中の場合)コミットもしくはロールバックするのを待ちます。 最初の更新処理がロールバックされるとその結果は無視されて、2番目の更新処理で元々検出した行の更新を続行することができます。 最初の更新処理がコミットされると、2番目の更新処理では、最初の更新処理により行が削除された場合はその行を無視します。 行が削除されなかった時の更新処理は、最初のコミットで更新された行に適用されます。 コマンドの検索条件 (WHERE句)は、更新された行がまだその検索条件に一致するかどうかの確認のため再評価されます。 検索条件と一致している場合、2 番目の更新処理は、更新された行から処理を開始します。
このような仕組みにより、更新コマンドが、たがいに矛盾したスナップショットを参照する可能性があります。 これらの問い合わせは、たがいが更新しようとしている同じ行に影響を及ぼす同時実行中の更新問い合わせによる結果を参照できますが、データベース中の他の行に対する同時実行の問い合わせの結果は参照できません。 このような動作をするために複合検索条件を含む問い合わせにリードコミッティドモードを使用することは適切ではありません。 しかし、より単純な検索条件の場合、このモードの使用が適しています。 たとえば、銀行の残高を更新する以下のようなトランザクションを考えてみます。
BEGIN; UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 12345; UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 7534; COMMIT;
同時に実行される2つのトランザクションが、口座番号12345の残高を変更しようとした場合、口座12345の行が更新されてから2番目のトランザクションを開始すべきです。 各コマンドが事前に決定されていた行にのみ処理を行なうため、更新されたバージョンの行は問題となる不整合を引き起こしません。
リードコミッティドモードでは、新規の各コマンドは、その時点でコミットされているすべてのトランザクションを含めた新規のスナップショットを使用して実行を開始するため、同一トランザクション内の後続のコマンドでは、いかなる場合でも、コミットされた同時実行中のトランザクションの結果を参照することになります。 この問題でのポイントは、単一のコマンド内で、完全に整合しているデータベースのビューを参照しているかどうかということです。
リードコミッティドモードで提供されている部分的なトランザクション隔離は、多くのアプリケーションでは適切です。 またこのモードは高速で、使い方も簡単です。 しかし、複雑な問い合わせや更新を行うアプリケーションでは、リードコミッティドモードで提供される以上に、データベースに対して厳密に一貫性のある見え方を保障する必要があるかもしれません。
シリアライザブルレベルは、トランザクションの隔離としては最も厳密なものです。 このレベルではトランザクションが同時にではなく、次から次へと、あたかも順に実行されているように遂次的なトランザクションの実行をエミュレートします。 しかし、このレベルを使ったアプリケーションでは、直列化の失敗によるトランザクションの再実行に備えておく必要があります。
トランザクションがシリアライザブル隔離レベルにあるときにSELECT問い合わせを実行すると、トランザクションが開始される前までにコミットされたデータのみを参照します。 コミットされていないデータや、そのトランザクションの実行中に別のトランザクションで更新されたデータは参照しません。 (しかし、SELECT文では、そのトランザクションで行われた、まだコミットされていないデータを参照します。) SELECT文では、トランザクション内のこの問い合わせが行われ始めた時点ではなく、トランザクションそのものが始まったときの状態のスナップショットを参照するという点でリードコミッティドレベルとは異なっています。 したがって、単一トランザクション内の連続するSELECT文は、常に同じデータを参照していることになります。
UPDATE、DELETE、およびSELECT FOR UPDATEコマンドでは、SELECTと同じように対象行を検索します。 これらのコマンドでは、トランザクションが開始された時点でコミットされている対象行のみを検出します。 しかし、その対象行は、検出されるまでに、同時実行中の他のトランザクションによって、すでに更新 (もしくは削除、もしくは更新対象としてマーク)されている可能性があります。 このような場合、シリアライザブルトランザクションは、最初の更新トランザクションが(それらがまだ進行中の場合)コミットもしくはロールバックするのを待ちます。 最初の更新処理がロールバックされると、その結果は無視され、シリアライザブルトランザクションでは元々検出した行の更新を続行することができます。 しかし、最初の更新処理がコミット(かつ、単に更新のために選択されるだけでなく、実際に行が更新または削除)されると、シリアライザブルトランザクションでは、以下のようなメッセージを出力してロールバックを行ないます。
ERROR: could not serialize access due to concurrent update
これは、シリアライザブルトランザクションでは、トランザクションが開始された後に別のトランザクションによって更新されたデータは変更できないためです。
アプリケーションがこのエラーメッセージを受け取った場合、現在のトランザクションを中断して、トランザクション全体を始めからやり直されなければなりません。 2回目では、トランザクションはコミットされた変更含めてをデータベースを最初の状態とみなすので、新しいバージョンの行を新しいトランザクションにおける更新の始点としても、論理的矛盾は起こりません。
更新トランザクションのみ再実行する必要があるかもしれません。 読み込み専用トランザクションでは直列化の衝突は決して起こりません。
シリアライザブルモードでは、すべてのトランザクションが一貫したデータベースの状態を参照できることが保障されます。 しかし、同時にトランザクションの更新を行うことで、今までずっと逐次実行しているように見せかけてきたものが破綻してしまいそうな場合、アプリケーションではトランザクションを再実行する準備をしておく必要があります。 複雑なトランザクションを再実行する際のコストが無視できないほど大きくなる可能性があるため、このモードは、リードコミッティドモードでは誤った結果を表示させてしまう可能性がある、かなり複雑なロジックを有する更新トランザクションを実行する場合にのみ使用することをお勧めします。 ほとんどの場合、シリアライザブルモードは、データベースの同一ビューを参照する必要のある複数の連続する問い合わせをトランザクションが処理する際に必要です。