9.2. トランザクションの隔離

SQL 標準では、同時に実行されるトランザクション間で防止すべき 3 つの現象ごとに、トランザクションの隔離レベルを 4 レベルに分けて定義しています。 3 つの望ましくない現象とは、下記のものです。

ダーティリード

同時に実行されている他のトランザクションが書き込んだ、まだコミットされていないデータを読み込んでしまう。

反復不能読み取り

トランザクションが、以前読み込んだデータを再度読み込む際、そのデータを別のトランザクションが更新しコミットしたことによって、以前のデータではなく、新しいデータを得てしまう。

ファントムリード

トランザクションが、ある行の集合を返す検索条件で問い合わせを再実行したとき、別のトランザクションがその問い合わせ条件を満たす行を変更し、コミットしてしまったために、同じ検索条件で問い合わせを実行しても異なる結果を得てしまう。

4つの隔離レベルとその Table 9-1で示した動作との対応は次の表のようになります。

Table 9-1. SQL トランザクション隔離レベル

隔離レベル ダーティリード 反復不能読み取り ファントムリード
Read uncommitted 可能性あり 可能性あり 可能性あり
Read committed 安全 可能性あり 可能性あり
Repeatable read 安全 安全 可能性あり
Serializable 安全 安全 安全

PostgreSQLはRead CommittedとSerializableの2つの隔離レベルを備えています。

9.2.1. リードコミッティド隔離レベル

PostgreSQLでは、Read Committedがデフォルトの隔離レベルに設定されています。 トランザクションをこの隔離レベルで実行する場合、SELECT 問い合わせを実行すると、問い合わせが実行される前までにコミットされたデータのみを参照します。コミットされていないデータや、その問い合わせの実行中に別の同時実行トランザクションでコミットされた更新は参照しません。 (しかし、SELECT 文では、そのトランザクションで行われた、まだコミットされていないデータを参照します)。 つまり、SELECT 文を使用した問い合わせでは、その問い合わせが実行を開始した時のデータベースのスナップショットを参照していることになります。 単一のトランザクション内であっても、SELECT 文を 2 回連続して発行した場合、最初の SELECT 文を処理している最中に他のトランザクションが更新をコミットすると、2 回のそれぞれの SELECT 文は異なるデータを参照することに注意して下さい。

UPDATEDELETE、および 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 の残高を変更しようとした場合、口座番号の行が更新されてから 2 番目のトランザクションが開始されるべきです。 各問い合わせが事前に決定されていた行にのみ処理を行なうため、更新されたバージョンの行は問題のある不整合を引き起こしません。

リードコミッティドモードでは、新規の各問い合わせは、その時点でコミットされているすべてのトランザクションを含めた新規のスナップショットを使用して実行を開始するため、同一トランザクション内の後続の問い合わせでは、いかなる場合でも、コミットされた同時実行中のトランザクションの結果を参照することになります。 この問題でのポイントは、単一の問い合わせ内で、完全に整合しているデータベースのビューを参照しているかどうかということです。

リードコミッティドモードで提供されている部分的なトランザクション隔離は、多くのアプリケーションにとって十分なものです。このモードは高速で、使用も容易です。 しかし、複雑な問い合わせや更新を行うアプリケーションでは、リードコミッティドモードで提供される以上に、データベースに対して厳密に一貫性のある見え方を保障する必要があるかもしれません。

9.2.2. シリアライザブル隔離レベル

シリアライザブルは、トランザクションの隔離としては最も厳密なものです。このレベルではトランザクションが同時にではなく、次から次へと、あたかも順に実行されているように遂次的なトランザクションの実行を模倣します。しかし、このレベルを使ったアプリケーションでは、直列化(シリアライゼーション)の失敗によるトランザクションの再実行に備えておく必要があります。

トランザクションがシリアライザブル隔離レベルにあるときに SELECT 文を実行すると、トランザクションが開始される前までにコミットされたデータのみを参照します。コミットされていないデータや、そのトランザクションの実行中に別のトランザクションで更新されたデータは参照しません。 (しかし、SELECT 文では、そのトランザクションで行われた、まだコミットされていないデータを参照します)。SELECT文では、トランザクション内のこの問い合わせが行われ始めた状態ではなく、トランザクションそのものの始まったときの状態のスナップショットを参照するという点でリードコミッティドレベルとは異なっています。 したがって、単一トランザクション内の連続する SELECT 文は、常に同じデータを参照していることになります。

UPDATEDELETE、および SELECT FOR UPDATE コマンドでは、SELECT と同じように対象行を検索します。 これらのコマンドでは、トランザクションが開始された時点でコミットされている対象行のみを検出します。 しかし、その対象行は、検出されるまでに、同時実行中の他のトランザクションによって、すでに更新 (もしくは削除、もしくは更新対象としてマーク) されている可能性があります。 このような場合、シリアライザブルトランザクションは、最初の更新トランザクションが (それらがまだ進行中の場合) コミットもしくはロールバックするのを待ちます。 最初の更新処理がロールバックされると、その結果は無視され、シリアライザブルトランザクションでは元々検出した行の更新を続行することができます。 しかし、最初の更新処理がコミット (かつ、単に更新のために選択されるだけでなく、実際に行が更新または削除) されると、シリアライザブルトランザクションでは、以下のようなメッセージを出力してロールバックを行ないます。

ERROR:Can't serialize access due to concurrent update

これは、シリアライザブルトランザクションでは、トランザクションが実行された後に別のトランザクションによって更新されたデータは変更できないためです。

アプリケーションがこのエラーメッセージを受け取った場合、現在のトランザクションを中断して、トランザクション全体を始めからやり直されなければなりません。2回目では、トランザクションはコミットされた変更含めてをデータベースの最初の状態とみなすので、新しいバージョンの行を新しいトランザクションにおける更新の始点としても、論理的矛盾は起こりません。

更新トランザクションのみ再実行する必要があります。読み込み専用トランザクションでは直列化 (シリアライゼーション) の衝突は決して起こりません。

シリアライザブルモードでは、すべてのトランザクションが一貫したデータベースの状態を参照できることが保障されます。 しかし、同時にトランザクションの更新を行うことで、今までずっと逐次実行しているように見せかけてきたものが破綻してしまいそうな場合、アプリケーションはトランザクションを再実行する準備をしておく必要があります。 複雑なトランザクションを再実行する際のコストが、無視できないほど大きくなる可能性があるため、このモードは、リードコミッティドモードでは誤った結果を表示させてしまう可能性がある、かなり複雑なロジックを有する更新トランザクションを実行する場合にのみ使用することをお勧めします。 ほとんどの場合、シリアライザブルモードは、データベースの同一ビューを参照する必要のある複数の連続する問い合わせをトランザクションが処理する際に必要です。