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の残高を変更しようとした場合、口座の行の更新されたバージョンに対して2番目のトランザクションが開始されることは明らかに望まれるところです。 各コマンドは事前に決定していた行に対してのみ処理を行うため、行の更新されたバージョンを見せることによって、何の問題となる不整合も引き起こしません。
より複雑な使用法により、リードコミッティドモードでは好ましくない結果を生成する場合があります。
例えば、別のコマンドによってDELETE
の制約条件からデータが同時に追加・削除される場合を考えます。
例えば、website
は2行のテーブルで、website.hits
の値には9
と10
があるとします。
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つを参照することはできません。 この分離レベルで実行するトランザクションによりビジネスルールを強制しようとすることは、競合するトランザクションをブロックするために注意深く明示的なロックを持たないと、正確に動作しないことが多くあります。
リピータブルリード分離レベルは、学術的なデータベースの文献や他のデータベース製品のいくつかではスナップショット分離として知られる技術を用いて実装されています。 同時実行性の面で劣る伝統的なロック技術を使うシステムと比較すると振舞いや性能の違いが観察されるかもしれません。 他のシステムでは、リピータブルリードとスナップショット分離を異なる振舞いをする別の分離レベルとして提供しているかもしれません。 2つの技術を区別する許容される現象は、標準SQLが制定されるまではデータベース研究者により定式化されておらず、この文書の範囲を超えます。 詳細な取り扱いについては[berenson95]を参照してください。
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で計算される総和が異なる結果になるからです。
異常を防止するためにシリアライザブルトランザクションを使用するのであれば、恒久的なユーザテーブルから読み取られたいかなるデータも、それを読んだトランザクションがコミットされるまで有効とは認められない点は重要です。 このことは読み取り専用トランザクションにも当てはまりますが、遅延可能な読み取り専用トランザクション内で読み込まれたデータは例外で、読み込まれてすぐに有効とみなされます。 なぜなら、遅延可能なトランザクションはすべてのデータを読み込む前にこのような問題がないことを保証されているスナップショットを取得できるまで待機するからです。 それ以外の全ての場合において、後に中止されたトランザクション内で読み込まれた結果をアプリケーションは信用してはならず、アプリケーションはトランザクションが成功するまで再試行すべきです。
真の直列性を保証するためにPostgreSQLでは、述語ロックを使います。
述語ロックでは、トランザクションが最初に実行されたとしたら、それによる書き込みが同時実行トランザクションによる読み取り結果にいつ影響を及ぼしたかの決定を可能にするロックを保持します。
PostgreSQLでは、これらのロックはブロッキングを引き起こさないため、デッドロックの要因とならないものです。
それらは、同時実行中のシリアライザブルトランザクションが、直列化異常につながる組み合わせであることを識別しフラグを立てることに使用されます。
それとは対照的に、データの一貫性を保証したいリードコミッティドあるいはリピータブルリードトランザクションでは、テーブル全体のロック(そのテーブルを使用しようとしている他のユーザをブロックするかもしれません)を必要とするかもしれませんし、あるいは、他のトランザクションをブロックするだけでなくディスク・アクセスを引き起こすSELECT FOR UPDATE
あるいはSELECT FOR SHARE
を使用するかもしれません。
PostgreSQLの述語ロックは、他のほとんどのデータベースシステムと同様、トランザクションによって実際にアクセスされたデータを元にしています。
これらは、pg_locks
システムビューにmode
がSIReadLock
のデータとして現れます。
問い合わせの実行期間中に獲得される個別のロックは、問い合わせが使用した計画に依存するでしょう。
また、ロックを追跡するために使用されるメモリの消耗を防ぐために、トランザクションの過程において、多数のよりきめの細かいロック(例えばタプル・ロック)が結合されて、より少数のよりきめの粗いロック(例えばページ・ロック)になるかもしれません。
直列化異常につながるような競合が継続して生じないことを検知すると、READ ONLY
トランザクションは、それが完了する前にSIReadロックを解放できるかもしれません。
実際、READ ONLY
トランザクションは、よく開始時点でその事実を確証し、どんな述語ロックもとらないこともあります。
SERIALIZABLE READ ONLY DEFERRABLE
トランザクションを明示的に要求した場合には、この事実を確証できるまでブロックします。
(これは、シリアライザブルトランザクションはブロックするけれども、リピータブルリードトランザクションはブロックしない唯一のケースです。)
他方で、SIReadロックは、しばしば読み取りと書き込みが重なっているトランザクションが完了するまで、トランザクションのコミットが終わっても保持される必要があります。
シリアライザブルトランザクションの一貫した使用は開発を単純化することができます。
正常にコミットされた同時実行のシリアライザブルトランザクションのどんな集合も、あたかもそれらが一度に一つずつ実行されたのと同じ結果になることが保証されるので、単独で実行されたときに単一トランザクションが正しく動作するよう書かれていると実証できるなら、他のトランザクションが何をしているかの情報が全く無くとも、複数シリアライザブルトランザクションが混在する中で正しく動作するかコミットに成功しないかであると確証を持つことができます。
この技術を使用する環境では、直列化の失敗(常にSQLSTATE値が'40001'で返る)を扱うための、汎用的な手段を持っていることが重要です。
なぜなら、どのトランザクションが読み取り/書き込みの依存性に影響し、直列化異常を防ぐためにロールバックさせる必要があるかといこうとを、正確に予測することは非常に困難だからです。
読み取り/書き込みの依存性を監視したり、直列化異常で終了したトランザクションを再起動することはコストがかかります。
しかしながら、このコストと、明示的なロックとSELECT FOR UPDATE
またはSELECT FOR SHARE
を使用したブロッキングとで比較検討すると、シリアライザブルトランザクションはいくつかの環境において最良な実行を選択することになります。
PostgreSQLのシリアライザブルトランザクション隔離レベルが同じ結果を生む実行順序があることを証明できるときだけ、同時のトランザクションのコミットを許すとはいえ、本当のシリアル実行では起こらないエラーが常に防げるわけではありません。 特に、たとえそのキーが生成されていないことを挿入しようとする前に明示的に調査した後でも重複しているシリアライザブルトランザクションとの競合が原因で一意性制約違反を見ることになる可能性があります。 これは潜在的に競合しているキーを挿入する全てのシリアライザブルトランザクションで確実に挿入できるかどうか最初に明示的に調査することで防ぐことができます。 例えば、ユーザに新しいキーを聞いてからまずselectでそれがすでに存在しているか確かめるアプリケーション、もしくは存在している中で一番大きなキーを選択しそれに1を足すことで新しいキーを生成するアプリケーションを想像してみてください。 もしいくつかのシリアライザブルトランザクションがこのプロトコルに沿わずに直接新しいキーを挿入すれば、たとえそれがシリアル実行の同時トランザクションでは起こりえないケースでも一意性制約違反が報告されることになります。
同時実行制御のためにシリアライザブルトランザクションを使用する場合、最適な性能のためには、以下の問題を考慮すべきです。
可能であればトランザクションをREAD ONLY
として宣言してください。
もし必要ならばコネクションプールを使用して、活動中の接続数を制御してください。 これは常に重要な性能上の考慮点ですが、シリアライザブルトランザクションを使用した多忙なシステムにおいては、特に重要になる可能性があります。
完全性のために必要とされる以上のものを1つのトランザクションに入れないようにしてください。
必要以上に長く「トランザクション内で待機状態」で接続したまま放置しておかないようにしてください。 長引くセッションを自動的に切断するために、設定パラメータidle_in_transaction_session_timeoutを使うことができます。
シリアライザブルトランザクションにより自動的に提供される保護により、不必要な、明示的なロック、SELECT FOR UPDATE
およびSELECT FOR SHARE
を取り除いてください。
述語ロックのテーブルがメモリ不足になると、複数のページレベルの述語ロックを単一のリレーションレベルの述語ロックへと結合するようシステムが強いられ、直列化失敗の発生割合が増加する恐れがあります。 これは、max_pred_locks_per_transaction、max_pred_locks_per_relation、max_pred_locks_per_pageのいずれか、あるいは、すべてを増やすことにより回避することができます。
シーケンシャルスキャンは常にリレーションレベルでの述語ロックを必要とします。 これによって、直列化失敗の頻度が増える可能性があります。 random_page_costを縮小および(または)cpu_tuple_costを増加することによりインデックススキャンの使用を促進することは有用かもしれません。 トランザクションのロールバックや再実行の減少を、問い合わせ実行時間の全体的な変化と比較検討するようにしてください。
シリアライザブル分離レベルは、学術的なデータベースの文献ではシリアライザブルスナップショット分離として知られる技術を使って実装されています。シリアライザブルスナップショット分離は、スナップショット分離の上に直列化異常の確認を追加することで構築されています。 伝統的なロック技術を使う他のシステムと比較すると振舞いや性能の違いが観察されるかもしれません。 詳細な情報は[ports12]を参照してください。