SQL 標準では、同時に実行されるトランザクション間で防止されるべき 3 つの現象に対してトランザクションの隔離レベルを 4 レベルに分けて定義しています。 3 つの望ましくない現象とは下記のものです。
4つの隔離レベルとその Table 9-1で示した動作との対応は次の表のようになります。
Table 9-1. SQL トランザクション隔離レベル
隔離レベル | ダーティリード | 反復不能読み取り | ファントムリード |
---|---|---|---|
Read uncommitted | 可能性あり | 可能性あり | 可能性あり |
Read committed | 安全 | 可能性あり | 可能性あり |
Repeatable read | 安全 | 安全 | 可能性あり |
Serializable | 安全 | 安全 | 安全 |
PostgreSQLはRead CommittedとSerializableの2つの隔離レベルを備えています。
PostgreSQLでは、Read Committedがデフォルトの隔離レベルです。 トランザクションがこの隔離レベルで実行されると、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:Can't serialize access due to concurrent update
これは、シリアライザブルトランザクションでは、トランザクションが実行された後に別のトランザクションによって更新されたデータは変更できないためです。
アプリケーションがこのエラーメッセージを受け取った場合、現在のトランザクションを中断して、トランザクション全体を始めからやり直されなければなりません。2回目では、トランザクションはコミットされた変更含めてをデータベースを最初の状態とみなすので、新しいバージョンの行を新しいトランザクションにおける更新の始点としても、論理的矛盾は起こりません。
更新トランザクションのみ再実行する必要があります。読み込み専用トランザクションでは直列化 (シリアライゼーション) の衝突は決して起こりません。
シリアライザブルモードでは、すべてのトランザクションが一貫したデータベースの状態を参照できることが保障されます。 しかし、同時にトランザクションの更新を行うことで、今までずっと逐次実行しているように見せかけてきたものが破綻してしまいそうな場合、アプリケーションではトランザクションを再実行する準備をしておく必要があります。 複雑なトランザクションを再実行する際のコストが無視できないほど大きくなる可能性があるため、このモードは、リードコミッティドモードでは誤った結果を表示させてしまう可能性がある、かなり複雑なロジックを有する更新トランザクションを実行する場合にのみ使用することをお勧めします。 ほとんどの場合、シリアライザブルモードは、データベースの同一ビューを参照する必要のある複数の連続する問い合わせをトランザクションが処理する際に必要です。