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

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

ダーティリード

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

反復不能読み取り

トランザクションが、以前読み込んだデータを再度読み込み、そのデータが別のトランザクションによって更新されたことを見出す (別のトランザクションが読み込み間隔の間にコミットした)。

ファントムリード

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

4つの隔離レベルとその動作を表12-1に示します。

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

隔離レベル ダーティリード 反復不能読み取り ファントムリード
リードアンコミッティド 可能性あり 可能性あり 可能性あり
リードコミッティド 安全 可能性あり 可能性あり
リピータブルリード 安全 安全 可能性あり
シリアライザブル 安全 安全 安全

PostgreSQLでは、4つの標準トランザクション隔離レベルを全て要求することができます。 しかし、内部的には、リードコミッティドとシリアライザブルに対応する2つの独立した隔離レベルのみがあります。 リードアンコミッティドレベルを選択した時、実際にはリードコミッティドになり、リピータブルリードを選択した時、実際にはシリアライザブルになります。 このように実際の隔離レベルは選択したレベルより厳密になることがあります。 これは標準SQLでも許されています。 この4つの隔離レベルについては、発生してはならない事象のみが定義され、発生しなければならない事象は定義されていません。 PostgreSQLが2つの隔離レベルのみを提供している理由は、多版型同時実行制御という仕組みに標準の隔離レベルを関連付ける実際的な方法がこれしかなかったことです。 利用可能な隔離レベルについては後で詳細に説明します。

トランザクションのトランザクション隔離レベルを設定するにはSET TRANSACTIONコマンドを使用してください。

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

PostgreSQLでは、リードコミッティドがデフォルトの隔離レベルです。 トランザクションがこの隔離レベルで実行されると、SELECT問い合わせはその問い合わせが実行される直前までにコミットされたデータのみを参照し、未だコミットされていないデータや、その問い合わせの実行中に別の同時実行トランザクションがコミットした更新は参照しません (とは言っても、SELECT文は、自分自身のトランザクション内で実行され更新された結果はたとえまだコミットされていなくても参照します)。 結果として、SELECT問い合わせはその問い合わせが実行を開始した時点のデータベースのスナップショットを参照することになります。 単一のトランザクション内であっても、SELECT文を2回連続して発行した場合、最初のSELECT文を処理している最中に他のトランザクションが更新をコミットすると、最初とその次に発行したSELECT問い合わせは異なるデータを参照してしまうことを知っておいてください。

UPDATEDELETESELECT FOR UPDATE、およびSELECT FOR SHAREコマンドは対象行を検索する際にSELECTコマンドと同じように振舞います。 これらのコマンドは、問い合わせが開始された時点で既にコミットされている対象行のみを検出します。 しかし、その対象行は、検出されるまでに、同時実行中の他のトランザクションによって、既に更新(もしくは削除あるいはロック)されてしまっているかもしれません。 このような場合更新されるべき処理は、最初の更新トランザクションが(それがまだ進行中の場合)コミットもしくはロールバックするのを待ちます。 最初の更新処理がロールバックされるとその結果は無視されて、2番目の更新処理で元々検出した行の更新を続行することができます。 最初の更新処理がコミットされると、2番目の更新処理では、最初の更新処理により行が削除された場合はその行を無視します。 行が削除されなかった時の更新処理は、最初のコミットで更新された行に適用されます。 コマンドの検索条件(WHERE句)は、更新された行がまだその検索条件に一致するかどうかの確認のため再評価されます。 検索条件と一致している場合、2番目の更新処理は、更新された行から処理を開始します。 (SELECT FOR UPDATEおよびSELECT FOR SHAREの場合、ロックされクライアントに返されるのは、更新されるバージョンの行であることを意味します。)

このような仕組みにより、更新コマンドが、互いに矛盾したスナップショットを参照する可能性があります。 これらの問い合わせは、互いが更新しようとしている同じ行に影響を及ぼす同時実行中の更新問い合わせによる結果を参照できますが、データベース中の他の行に対する同時実行の問い合わせの結果は参照できません。 このような動作をするために複合検索条件を含む問い合わせにリードコミッティドモードを使用することは適切ではありません。 しかし、より単純な検索条件の場合、このモードの使用が適しています。 例えば、銀行の残高を更新する以下のようなトランザクションを考えてみます。

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番目のトランザクションを開始すべきです。 各コマンドが事前に決定していた行にのみ処理を行うため、更新されたバージョンの行は問題となる不整合を引き起こしません。

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

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

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

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

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

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

ERROR:  could not serialize access due to concurrent update

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

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

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

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

12.2.2.1. シリアライザブル隔離と本当の直列性

"シリアライザブル"な実行の直感的な意味(および数学的な定義)は、コミットに成功した2つの同時実行のトランザクションは、厳密に直列的に、つまり1つひとつ順番に実行されるように見えるということです。 しかし、どちらが先に現れるかを前もって予測することができません。 表12-1に示した直列化できない振舞いを禁止することが真の直列性を完全に保証しないことを認識することは重要です。 そして、実際PostgreSQLのシリアライザブルモードはこの点のため直列化実行を保証していません。 例えば、以下の初期データを持つmytabというテーブルを考えてみます。

 class | value 
-------+-------
     1 |    10
     1 |    20
     2 |   100
     2 |   200

ここでシリアライザブルモードのトランザクションAが以下を計算し、

SELECT SUM(value) FROM mytab WHERE class = 1;

そして、valueにその結果(30)を、class = 2の行を挿入したとします。 同時にシリアライザブルモードのトランザクションBが以下を計算し、

SELECT SUM(value) FROM mytab WHERE class = 2;

その結果300を得、そして、この結果をclass = 1の行として挿入します。 ここで両方のトランザクションがコミットします。 ここでは前述の直列化できない振舞いはありませんが、どちらを先に直列化したとしても得ることができない結果になります。 もしAがBより前に実行したとすると、Bの総和は300ではなく330と計算されます。 同様に別の順序で実行されたとしてもAで計算される総和は異なる結果になります。

数学的に正しい直列性を保証するには、データベースシステムは、同時実行トランザクションの問い合わせのWHERE条件に一致する行を、他のトランザクションからは挿入、変更ができないことを意味する述部ロック処理を強制的に行う必要があります。 例えば、トランザクションAがSELECT ... WHERE class = 1という問い合わせを行った時、述部ロック処理システムは、トランザクションAがコミットされるまで、トランザクションBがclassが1の行を新しく挿入することを禁止します。 [1] 全てのセッションが、全ての同時実行トランザクションで実行された全ての問い合わせの細部に注意しなければなりませんので、このようなロック処理システムの実装は複雑で、その実行はかなり高価です。 また、実際にはほとんどのアプリケーションはこうした問題が生じる可能性がある操作を行いませんので、こうした高価な作業の大部分は無駄になります (確かに上の例はわざと考えたものであり、実際のソフトウェアで行われる可能性はほぼありません)。 したがって、PostgreSQLは述部ロック処理を実装していません。

直列実行されない可能性があることが本当に障害になる場合は、明示的なロック処理を適切に使用することで問題を解決することができます。 以下の節でより詳細に説明します。

注意

[1]

基本的に、述部ロックシステムは書き込まれるものを制限することでファントムリードを防ぎ、MVCCは読み込まれるものを制限することでファントムリードを防ぎます。