標準SQLでは、同時に実行されるトランザクション間で防止されるべき3つの現象に対してトランザクションの隔離レベルを4レベルに分けて定義しています。 3つの望ましくない現象とは下記のものです。
4つの隔離レベルとその動作を表13-1に示します。
表 13-1. SQLトランザクション隔離レベル
隔離レベル | ダーティリード | 反復不能読み取り | ファントムリード |
---|---|---|---|
リードアンコミッティド | 可能性あり | 可能性あり | 可能性あり |
リードコミッティド | 安全 | 可能性あり | 可能性あり |
リピータブルリード | 安全 | 安全 | 可能性あり |
シリアライザブル | 安全 | 安全 | 安全 |
PostgreSQLでは、4つの標準トランザクション隔離レベルを全て要求することができます。 しかし、内部的には、リードコミッティドとシリアライザブルに対応する2つの独立した隔離レベルのみがあります。 リードアンコミッティドレベルを選択した時、実際にはリードコミッティドになり、リピータブルリードを選択した時、実際にはシリアライザブルになります。 このように実際の隔離レベルは選択したレベルより厳密になることがあります。 これは標準SQLでも許されています。 この4つの隔離レベルについては、発生してはならない事象のみが定義され、発生しなければならない事象は定義されていません。 PostgreSQLが2つの隔離レベルのみを提供している理由は、多版型同時実行制御という仕組みに標準の隔離レベルを関連付ける実際的な方法がこれしかなかったことです。 利用可能な隔離レベルについては後で詳細に説明します。
トランザクションのトランザクション隔離レベルを設定するにはSET TRANSACTIONコマンドを使用してください。
PostgreSQLでは、リードコミッティドがデフォルトの隔離レベルです。 トランザクションがこの隔離レベルを使用すると、SELECT問い合わせ(FOR UPDATE/SHARE句を伴わない)はその問い合わせが実行される直前までにコミットされたデータのみを参照し、未だコミットされていないデータや、その問い合わせの実行中に別の同時実行トランザクションがコミットした更新は参照しません。 結果として、SELECT問い合わせはその問い合わせが実行を開始した時点のデータベースのスナップショットを参照することになります。 とは言っても、SELECT文は、自分自身のトランザクション内で実行され更新された結果はたとえまだコミットされていなくても参照します。 単一のトランザクション内であっても、SELECT文を2回連続して発行した場合、最初のSELECT文を処理している最中に他のトランザクションが更新をコミットすると、最初とその次に発行したSELECT問い合わせは異なるデータを参照してしまうことにも注意してください。
UPDATE、DELETE、SELECT 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番目のトランザクションを開始することは明らかに望まれるところです。 各コマンドが事前に決定していた行にのみ処理を行うため、更新されたバージョンの行は問題となる不整合を引き起こしません。
より複雑な用法によりリードコミッティドモードでは好ましくない結果を生成します。例えば、別のコマンドによりその制約条件から追加・削除の両方が行われようとしているデータに作用するDELETEコマンドを考えます。例を挙げると、websiteは2行のテーブルで、そこに9 と 10の値を持つwebsite.hitsがあります。
BEGIN; UPDATE website SET hits = hits + 1; -- 別のセッションから DELETE FROM website WHERE hits = 10; を実行します COMMIT;
UPDATE前後にwebsite.hits = 10行が存在したとしてもDELETEは効果を生みません。なぜこうなるのかと言うと、事前更新された行の値9は読み飛ばされ、UPDATEが完了し、DELETEがロックを取得した時点では、新規行の値はもはや10ではなく11となり、判定基準にもはや一致しません。
リードコミッティドモードは、その時点までにコミットされた全てのトランザクションを含む新規スナップショットを伴うそれぞれのコマンドで開始するので、同一のトランザクション内でそれに続くコマンドは、いかなる場合でもコミットされた同時実行トランザクションの結果を参照します。 上記問題の要点は単一のコマンドがデータベースの厳密に一貫性のある見え方を目撃するか否かです。
リードコミッティドモードで提供されている部分的なトランザクション隔離は、多くのアプリケーションでは適切です。 またこのモードは高速で、使い方も簡単ですが、全ての場合に対して充分ではありません。複雑な問い合わせや更新を行うアプリケーションは、リードコミッティドモードが提供する以上のより厳正なデータベースの厳密に一貫性のある見え方を必要とします。
シリアライザブル隔離レベルは、トランザクションの隔離としては最も厳密なものです。 このレベルではトランザクションが同時にではなく、次から次へと、あたかも順に実行されているように逐次的なトランザクションの実行をエミュレートします。 しかし、このレベルを使ったアプリケーションでは、直列化の失敗によるトランザクションの再実行に備えておく必要があります。
トランザクションがシリアライザブル隔離レベルを使用している場合、SELECT問い合わせを実行すると、トランザクションが開始される前までにコミットされたデータのみを参照します。 コミットされていないデータや、そのトランザクションの実行中に別のトランザクションで更新されたデータは参照しません。 (しかし、まだコミットされていないとしても、その問い合わせはそのトランザクション自身内で行われた直前の更新を参照します。) シリアライザブルトランザクション内のその問い合わせは、トランザクション内の現行の問い合わせの開始時点ではなく、そのトランザクションの開始時点のスナップショットを参照するという点でリードコミッティドレベルとは異なっています。 従って、単一トランザクション内の連続するSELECT文は、常に同じデータを参照していることになります。つまり、その自身のトランザクションの後にコミットされた他のトランザクションによる変更を参照しません。(この動作はアプリケーションに報告するために理想的なものになり得ます。)
UPDATE、DELETE、SELECT FOR UPDATE、およびSELECT FOR SHAREコマンドでは、SELECTと同じように対象行を検索します。 これらのコマンドでは、トランザクションが開始された時点で既にコミットされている対象行のみを検出します。 しかし、その対象行は、検出されるまでに、同時実行中の他のトランザクションによって、既に更新(もしくは削除あるいはロック)されている可能性があります。 このような場合、シリアライザブルトランザクションは、最初の更新トランザクションが(それらがまだ進行中の場合)コミットもしくはロールバックするのを待ちます。 最初の更新処理がロールバックされると、その結果は無視され、シリアライザブルトランザクションでは元々検出した行の更新を続行することができます。 しかし、最初の更新処理がコミット(かつ、単にロックされるだけでなく、実際に行が更新または削除)されると、シリアライザブルトランザクションでは、以下のようなメッセージを出力してロールバックを行います。
ERROR: could not serialize access due to concurrent update
これは、シリアライザブルトランザクションでは、トランザクションが開始された後に別のトランザクションによって更新されたデータは変更またはロックすることができないためです。
アプリケーションがこのエラーメッセージを受け取った場合、現在のトランザクションを中断して、トランザクション全体を始めからやり直されなければなりません。 2回目では、トランザクションはコミットされた変更含めてデータベースの最初の状態とみなすので、新しいバージョンの行を新しいトランザクションにおける更新の始点としても、論理的矛盾は起こりません。
更新トランザクションのみ再実行する必要があるかもしれません。 読み込み専用トランザクションでは直列化の衝突は決して起こりません。
シリアライザブルモードでは、全てのトランザクションが一貫したデータベースの状態を参照できることが保証されます。 しかし、同時にトランザクションの更新を行うことで、今までずっと逐次実行しているように見せかけてきたものが破綻してしまいそうな場合、アプリケーションではトランザクションを再実行する準備をしておく必要があります。 複雑なトランザクションを再実行する際のコストが無視できないほど大きくなる可能性があるため、シリアライザブルモードは、リードコミッティドモードでは誤った結果を表示させてしまう可能性がある、かなり複雑なロジックを有する更新トランザクションを実行する場合にのみ使用することをお勧めします。 ほとんどの場合、シリアライザブルモードは、データベースの同一ビューを参照する必要のある複数の連続する問い合わせをトランザクションが処理する際に必要です。
"シリアライザブル"な実行の直感的な意味(および数学的な定義)は、コミットに成功した2つの同時実行のトランザクションは、厳密に直列的に、つまり1つひとつ順番に実行されるように見えるということです。 しかし、どちらが先に現れるかを前もって予測することができません。 表13-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は読み込まれるものを制限することでファントムリードを防ぎます。 |