SQLの標準規格では、トランザクションの分離について4つのレベルを定義しています。 標準規格で定義されているもののうち最も厳密なものはシリアライザブルです。 1セットのシリアライザブルなトランザクションを同時実行した場合には、ある順番でひとつずつそれらを実行した場合と同じ結果となることが保証されるものです。本文で詳しく述べます。 他の3レベルは、同時実行しているトランザクション間の相互作用に起因する、各レベルでは発生してはならない現象面に基づき定義されます。 標準規格のシリアライザブルの定義では、このレベルではこれらの現象が起こりえないと述べています。 (これは驚くことではありません。トランザクションの効果がひとつずつ実行された場合と一貫性を持たなければならないとしたら、相互作用によって発生した現象はどうやっても見つけ出すことはできないでしょう。)
各種レベルにおける禁止される現象を以下に示します。
同時に実行されている他のトランザクションが書き込んで未だコミットしていないデータを読み込んでしまう。
トランザクションが、以前読み込んだデータを再度読み込み、そのデータが(最初の読み込みの後にコミットした)別のトランザクションによって更新されたことを見出す。
トランザクションが、複数行のある集合を返す検索条件で問い合わせを再実行した時、別のトランザクションがコミットしてしまったために、同じ検索条件で問い合わせを実行しても異なる結果を得てしまう。
複数のトランザクションを正常にコミットした結果が、それらのトランザクションを1つずつあらゆる可能な順序で実行する場合とは一貫性がない。
標準SQLおよびPostgreSQLで実装されているトランザクション分離レベルを表13.1「トランザクション分離レベル」に示します。
表13.1 トランザクション分離レベル
分離レベル | ダーティリード | 反復不能読み取り | ファントムリード | 直列化異常 |
---|---|---|---|---|
リードアンコミッティド | 許容されるが、PostgreSQLでは発生しない | 可能性あり | 可能性あり | 可能性あり |
リードコミッティド | 安全 | 可能性あり | 可能性あり | 可能性あり |
リピータブルリード | 安全 | 安全 | 許容されるが、PostgreSQLでは発生しない | 可能性あり |
シリアライザブル | 安全 | 安全 | 安全 | 安全 |
PostgreSQLでは、4つの標準トランザクション分離レベルを全て要求することができます。 しかし、内部的には3つの分離レベルしか実装されていません。 つまり、PostgreSQLのリードアンコミッティドモードは、リードコミッティドのように動作します。 これは、PostgreSQLの多版型同時実行制御という仕組みに標準の分離レベルを関連付ける実際的な方法がこれしかないからです。
このテーブルはまた、PostgreSQLのリピータブルリードの実装ではファントムリードが起こらないことを示しています。 より厳密な動作をすることは標準SQLでも許されています。 つまり、この4つの分離レベルでは、発生してはならない事象のみが定義され、発生しなければならない事象は定義されていません。 利用可能な分離レベルでの動作については後で詳細に説明します。
トランザクションのトランザクション分離レベルを設定するにはSET TRANSACTIONコマンドを使用してください。
いくつかのPostgreSQLデータ型と関数はトランザクションの振る舞いに関して特別の規則があります。
特に、シーケンスに対しての変更は(従い、serial
を使用して宣言された列のカウンタ)は直後に全ての他のトランザクションで可視となり、変更を行ったトランザクションが中止されるとロールバックはできません。
9.16. シーケンス操作関数および8.1.4. 連番型を参照してください。
PostgreSQLではリードコミッティドがデフォルトの分離レベルです。
トランザクションがこの分離レベルを使用すると、SELECT
問い合わせ(FOR UPDATE/SHARE
句を伴わない)はその問い合わせが実行される直前までにコミットされたデータのみを参照し、まだコミットされていないデータや、その問い合わせの実行中に別の同時実行トランザクションがコミットした更新は参照しません。
結果として、SELECT
問い合わせはその問い合わせが実行を開始した時点のデータベースのスナップショットを参照することになります。
しかしSELECT
文は、自分自身のトランザクション内で実行され更新された結果はたとえまだコミットされていなくても参照します。
単一のトランザクション内であっても、SELECT
文を2回連続して発行した場合、最初のSELECT
文が開始した後で2番目のSELECT
文が開始する前に他のトランザクションが更新をコミットすると、最初とその次に発行したSELECT
問い合わせは異なるデータを参照してしまうことにも注意してください。
UPDATE
、DELETE
、SELECT FOR UPDATE
、およびSELECT FOR SHARE
コマンドは対象行を検索する際にSELECT
コマンドと同じように振舞います。
これらのコマンドは、問い合わせが開始された時点で既にコミットされた対象行のみを検出します。
しかし、その対象行は、検出されるまでに、同時実行中の他のトランザクションによって、既に更新(もしくは削除あるいはロック)されてしまっているかもしれません。
このような場合更新されるべき処理は、最初の更新トランザクションが(それがまだ進行中の場合)コミットもしくはロールバックするのを待ちます。
最初の更新処理がロールバックされるとその結果は無視されて、2番目の更新処理で元々検出した行の更新を続行することができます。
最初の更新処理がコミットされると、2番目の更新処理では、最初の更新処理により行が削除された場合はその行を無視します。
行が削除されなかった時の更新処理は、最初のコミットで更新された行に適用されます。
コマンドの検索条件(WHERE
句)は、更新された行がまだその検索条件に一致するかどうかの確認のため再評価されます。
検索条件と一致している場合、2番目の更新処理は、更新された行を使用して処理を開始します。
SELECT FOR UPDATE
およびSELECT FOR SHARE
の場合、ロックされクライアントに返されるのは、更新されるバージョンの行であることを意味します。
ON CONFLICT DO UPDATE
句のあるINSERT
は同じように動作します。
リードコミッティドモードでは、挿入を提案された各行は挿入または更新されます。
無関係なエラーが発生しなければ、それら2つの結果のうち1つが保証されます。
まだその結果がINSERT
に対して可視になっていない他のトランザクションに起因する競合では、慣習的な意味でそのコマンドに対して可視のバージョンの行が存在しないにも関わらず、UPDATE
句がその行に対して動作します。
ON CONFLICT DO NOTHING
句のあるINSERT
では、INSERT
のスナップショットに対してその結果が可視になっていない他のトランザクションの結果のために、行の挿入が処理されないかもしれません。
ここでも、問題になるのはリードコミッティドモードのときだけです。
このような仕組みにより、更新コマンドが、互いに矛盾したスナップショットを参照する可能性があります。 それは更新を試みている同じ行に対する同時実行更新の結果を参照しますが、データベース中の他の行に対する同時実行の問い合わせの結果は参照できません。 このような動作をするために複合検索条件を含む問い合わせにリードコミッティドモードを使用することは適切ではありません。 しかし、より単純な検索条件の場合、このモードの使用が適しています。 例えば、銀行の残高を更新する以下のようなトランザクションを考えてみます。
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
となり、判定基準にもはや一致しません。
リードコミッティドモードは、それぞれのコマンドをその時点までにコミットされた全てのトランザクションを含む新規スナップショットを伴って開始するので、同一のトランザクション内でそれに続くコマンドは、いかなる場合でもコミットされた同時実行トランザクションの結果を参照します。 上記問題の要点は単一のコマンドがデータベースの厳密に一貫性のある見え方を見るか否かです。
リードコミッティドモードで提供されている部分的なトランザクション分離は、多くのアプリケーションでは適切です。 またこのモードは高速で、使い方も簡単ですが、全ての場合に対して充分ではありません。 複雑な問い合わせや更新を行うアプリケーションは、リードコミッティドモードが提供する以上のより厳正なデータベースの厳密に一貫性のある見え方を必要とします。
リピータブルリード分離レベルは、トランザクションが開始される前までにコミットされたデータのみを参照します。 コミットされていないデータや、そのトランザクションの実行中に別のトランザクションでコミットされた変更を参照しません。 (しかし、まだコミットされていないとしても、その問い合わせはそのトランザクション自身内で行われた過去の更新を参照します。) これはSQLの標準規格で求められるものよりもより強く保証するもので、表13.1「トランザクション分離レベル」で述べている現象をすべて防ぎます。上で述べたように、これは標準規格によって特に許容されており、それぞれの分離レベルが提供しなくてはならない最小の保護が示されています。
リピータブルリードのトランザクション内の問い合わせは、トランザクション内の現在の文の開始時点ではなく、トランザクションの最初のトランザクション制御以外の文の開始時点のスナップショットを見る、という点でこのレベルはリードコミッティドと異なります。
従って、単一トランザクション内の連続するSELECT
文は、同じデータを参照します。つまり、自身のトランザクションが開始した後にコミットされた他のトランザクションによる変更を参照しません。
このレベルを使ったアプリケーションでは、直列化の失敗によるトランザクションの再実行に備えておく必要があります。
UPDATE
、DELETE
、SELECT FOR UPDATE
、およびSELECT FOR SHARE
コマンドでは、SELECT
と同じように対象行を検索します。
これらのコマンドでは、トランザクションが開始された時点で既にコミットされている対象行のみを検出します。
しかし、その対象行は、検出されるまでに、同時実行中の他のトランザクションによって、既に更新(もしくは削除あるいはロック)されている可能性があります。
このような場合、リピータブルリードトランザクションは、最初の更新トランザクションが(それらがまだ進行中の場合)コミットもしくはロールバックするのを待ちます。
最初の更新処理がロールバックされると、その結果は無視され、リピータブルリードトランザクションでは元々検出した行の更新を続行することができます。
しかし、最初の更新処理がコミット(かつ、単にロックされるだけでなく、実際に行が更新または削除)されると、リピータブルリードトランザクションでは、以下のようなメッセージを出力してロールバックを行います。
ERROR: could not serialize access due to concurrent update
これは、リピータブルリードトランザクションでは、トランザクションが開始された後に別のトランザクションによって更新されたデータは変更またはロックすることができないためです。
アプリケーションがこのエラーメッセージを受け取った場合、現在のトランザクションを中断して、トランザクション全体を始めからやり直されなければなりません。 2回目では、トランザクションはコミットされた変更を含めてデータベースの最初の状態とみなすので、新しいバージョンの行を新しいトランザクションにおける更新の始点としても、論理的矛盾は起こりません。
更新トランザクションのみ再実行する必要があるかもしれません。 読み込み専用トランザクションでは直列化の衝突は決して起こりません。
リピータブルリードモードでは、全てのトランザクションがデータベースの一貫した不変のビューの状態を参照することが保証されます。 しかし、このビューは常にいくつかの同じレベルの同時実行トランザクションの並列(一度に一つずつの)実行で一貫性を持つ必要はありません。 例えば、このレベルの読み取りのみのトランザクションは、バッチが完了したことを示すために更新された制御レコードを参照することができますが、 制御レコードのより以前のバージョンを読み取るため、論理的にそのバッチの一部となる詳細なレコードの1つを参照することはできません。 この分離レベルで実行するトランザクションによりビジネスルールを強制しようとすることは、競合するトランザクションをブロックするために注意深く明示的なロックを持たないと、正確に動作しないことが多くあります。
PostgreSQL version 9.1より前まででは、シリアライザブル分離レベルの要求はここで説明した通りの動作をそのまま提供していました。 以前のシリアライザブルの動作を維持するためには、リピータブルリードを要求しなければならなくなりました。
シリアライザブル分離レベルは、最も厳しいトランザクションの分離性を提供します。 このレベルではトランザクションが同時にではなく、次から次へと、あたかも順に実行されているように逐次的なトランザクションの実行を全てのコミットされたトランザクションに対しエミュレートします。 しかし、このレベルを使ったアプリケーションでは、リピータブルリードレベルと同様に、直列化の失敗によるトランザクションの再実行に備えておく必要があります。 実際、この分離レベルは、(ある時点で)逐次実行可能なすべてのトランザクションにおいて、シリアライザブルトランザクションの同時実行の組が一貫性のないような振る舞いをしていないか監視することを除き、リピータブルリードと全く同じ動きをします。 この監視では、リピータブルリードが示すものを越えてブロックすることはありませんが、監視によりいくらかのオーバーヘッドがあり、直列化異常を引き起こすような状態の検知は、直列化の失敗を引き起こすでしょう。
例えば、以下の初期データを持つ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
の新たな行として挿入したとします。
その後、両方のトランザクションがコミットを試みます。
もし一方の処理がリピータブルリード分離レベルで実行していれば、両方のコミットが許されるでしょう。
しかし、結果と一致する順序で実行されたものがないため、シリアライザブルトランザクションを使用した場合は、ひとつのトランザクションがコミットを許され、他方はこのメッセージとともにロールバックされることになります。
ERROR: could not serialize access due to read/write dependencies among transactions
この理由は、もしAがBよりも前に実行が完了していた場合、Bの総和は300ではなく330と計算されるためです。 同様に別の順序で実行されたとしてもAで計算される総和は異なる結果になります。
異常を防止するためシリアライザブルトランザクションを信頼するのであれば、トランザクションが有効にコミットされたとそのトランザクションが認められるまで、恒久的なユーザテーブルから読み取られたいかなるデータも有効とは認められない点は重要です。 読み込まれて直ぐにdeferrable読み取り専用トランザクション内で読み込まれたデータを除いて、このことは読み取り専用トランザクションであっても真実です。 何故かと言うと、それらのトランザクションは他のデータを読み込む前に言われる問題から免がれている保証付きのスナップショットを取得可能となるまで待機するからです。 その他の全ての場合、後に中断されるトランザクションにおいて読み込まれた結果をアプリケーションは信用してはならないことです。 その代わりとして、アプリケーションはトランザクションが成功するまで再試行すべきです。
真の直列性を保証するためにPostgreSQLでは、最初に走らせた、同時実行トランザクションより前に読み取った結果に対して、書き込みがいつ影響を及ぼしたかを断定可能にするロックを保持することを意味する、述語ロックを使います。
PostgreSQLでは、これらのロックはブロッキングを引き起こさないため、デッドロックの要因とならないものです。
それらは、同時実行中のシリアライザブルトランザクションが、直列化異常につながる組み合わせであることを識別しフラグを立てることに使用されます。
それとは対照的に、データの一貫性を保証したいリードコミッティドあるいはリピータブルリードトランザクションでは、(そのテーブルを使用しようとしている他のユーザをブロックすることができた)テーブル全体のロックを必要とするかもしれません。あるいは、他のトランザクションをブロックするだけでなくディスク・アクセスを引き起こすSELECT FOR UPDATE
あるいはSELECT FOR SHARE
を使用するかもしれません。
PostgreSQLの述語ロックは、他のほとんどのデータベースシステムと同様、トランザクションによって実際にアクセスされたデータを元にしています。
これらは、SIReadLock
のmode
を持つpg_locks
システムビューで見ることができます。
問い合わせの実行期間中に獲得した特殊なロックは、問い合わせが使用した計画に依存するでしょう。また、ロックを追跡するために使用されるメモリの消耗を防ぐために、多数のよりきめの細かいロック(例えばタプル・ロック)はトランザクションの間に、より少数のよりきめの粗いロック(例えばページ・ロック)へ組み合わせられるかもしれません。
直列化異常につながるような競合が継続して生じないことを検知すると、READ ONLY
トランザクションが完了する前にSIReadロックを解除するかもしれません。
実際、READ ONLY
トランザクションは、よく開始時点でその事実を確証し、どんな述語ロックもとらないこともあります。SERIALIZABLE READ ONLY DEFERRABLE
トランザクションを明示的に要求した場合には、この事実を確証できるまでブロックします。(これは、シリアライザブルトランザクションはブロックするけれども、リピータブルリードトランザクションはブロックしない唯一のケースです。)
他方で、SIReadロックは、しばしば読み取りと書き込みが重なっているトランザクションが完了するまで、過去のトランザクションのコミットに保持される必要があります。
シリアライザブルトランザクションの一貫した使用は開発を単純化することができます。
同時実行したシリアライザブルトランザクションのどんなセットも同じ効果があるという保証は、あたかも、それらが一度に実行されたものだったかのように、単一のトランザクションとして証明できることを意味します。
これは、単独で実行された場合に正しく動作するよう書かれている場合、シリアライザブルトランザクションが混在していても正しく動作する確証を持つことができます。
この技術を使用する環境で、直列化の失敗('40001'のSQLSTATEの値で常に返る)を扱う場合、一般的な手段を持っていることは重要です。
なぜなら、トランザクションが読み取り/書き込みの依存性にどれだけ影響する可能性があるかということと、直列化異常を防ぐためにロールバックさせる必要があるかといこうとを、正確に予測することは非常に困難だからです。
読み取り/書き込みの依存性を監視したり、直列化異常で終了したトランザクションを再起動することはコストがかかります。
しかしながら、このコストと、明示的なロックとSELECT FOR UPDATE
またはSELECT FOR SHARE
を使用したブロッキングとで比較検討すると、シリアライザブルトランザクションはいくつかの環境において最良な実行を選択することになります。
同時実行制御のためにシリアライザブルトランザクションを使用する場合、最適に実行するためには、以下の問題を考慮すべきです。
できる限りREAD ONLY
として宣言してください。
もし必要ならばコネクションプールを使用して、活動中の接続数を制御してください。 これは常に重要な実行時の考慮点ですが、シリアライザブルトランザクションを使用した多忙なシステムにおいては、特に重要になる可能性があります。
完全性を目的とした必要以上の単一トランザクションを置かないようにしてください。
必要以上に長く「トランザクション内で待機状態」で接続したまま放置しておかないようにしてください。
シリアライザブルトランザクションにより自動的に提供される保護により、不必要な、明示的なロック、SELECT FOR UPDATE
およびSELECT FOR SHARE
を取り除いてください。
システムが、単一の関係レベルでの述語ロックに、多数のページレベルでの述語ロックを組み合わせることを強いられる場合、述語ロックのテーブルはメモリが不足するため、直列化失敗の割合が増加する恐れがあります。 これは、max_pred_locks_per_transactionを増やすことにより回避することができます。
シーケンシャルスキャンは常にリレーションレベルでの述語ロックを必要とします。これによって、直列化失敗の頻度が増える可能性があります。 random_page_costを縮小および(または)cpu_tuple_costを増加することによりインデックススキャンの使用を促進することは有用かもしれません。 問い合わせ実行時間の全体的な変化に不利となる、トランザクションのロールバックや再起動を減少させるように、必ず検討してください。