トランザクションの分離

PostgreSQL 9.1.1文書
前のページ | 巻戻し | 第 13章同時実行制御 | 早送り | 次のページ

# 13.2. トランザクションの分離

SQLの標準規格では、トランザクションの分離について4つのレベルを定義しています。 標準規格で定義されているもののうち最も厳密なものはシリアライザブルです。 1セットのシリアライザブルなトランザクションを同時実行した場合には、ある順番でひとつずつそれらを実行した場合と同じ結果となることが保証されるものです。本文で詳しく述べます。 他の3レベルは、同時実行しているトランザクション間の相互作用に起因する、各レベルでは発生してはならない現象面に基づき定義されます。 標準規格のシリアライザブルの定義では、このレベルではこれらの現象が起こりえないと述べています。 (これは驚くことではありません。トランザクションの効果がひとつずつ実行された場合と一貫性を持たなければならないとしたら、相互作用によって発生した現象はどうやっても見つけ出すことはできないでしょう。)

禁止される現象には様々なレベルがあります。

ダーティリード

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

反復不能読み取り

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

ファントムリード

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

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

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

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

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

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

## 13.2.1. リードコミッティド分離レベル

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

より複雑な用法によりリードコミッティドモードでは好ましくない結果を生成します。例えば、別のコマンドによりその制約条件から追加・削除の両方が行われようとしているデータに作用する`DELETE`コマンドを考えます。例を挙げると、`website`は２行のテーブルで、そこに`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`となり、判定基準にもはや一致しません。

リードコミッティドモードは、その時点までにコミットされた全てのトランザクションを含む新規スナップショットを伴うそれぞれのコマンドで開始するので、同一のトランザクション内でそれに続くコマンドは、いかなる場合でもコミットされた同時実行トランザクションの結果を参照します。上記問題の要点は単一のコマンドがデータベースの厳密に一貫性のある見え方を目撃するか否かです。

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

## 13.2.2. リピータブルリード分離レベル

リピータブルリード分離レベルは、トランザクションが開始される前までにコミットされたデータのみを参照します。 コミットされていないデータや、そのトランザクションの実行中に別のトランザクションでコミットされた変更を参照しません。 （しかし、まだコミットされていないとしても、その問い合わせはそのトランザクション自身内で行われた過去の更新を参照します。） これはSQLの標準規格で求められるものよりもより強く保証するもので、表13-1で述べている現象をすべて防ぎます。上で述べたように、これは標準規格によって特に許容されており、それぞれの分離レベルが提供しなくてはならない最小の保護が示されています。

このレベルでは、リピータブルリードトランザクションがトランザクション開始時点のスナップショットを見ます。トランザクション内の現在の問い合わせ開始時ではないという点で、リードコミッティドとは異なります。 従って、単一トランザクション内の連続する`SELECT`文は、常に同じデータを参照していることになります。つまり、その自身のトランザクションの後にコミットされた他のトランザクションによる変更を参照しません。

このレベルを使ったアプリケーションでは、直列化の失敗によるトランザクションの再実行に備えておく必要があります。

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

    ERROR:  could not serialize access due to concurrent update

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

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

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

リピータブルリードモードでは、全てのトランザクションがデータベースの一貫した不変のビューの状態を参照することが保証されます。 しかし、このビューは常にいくつかの同じレベルの同時実行トランザクションの並列（一度に一つずつの）実行で一貫性を持つ必要はありません。 例えば、このレベルの読み取りのみのトランザクションは、バッチが完了したことを示すために更新された制御レコードを参照することができますが、 制御レコードのより以前のバージョンを読み取るため、論理的にそのバッチの一部となる詳細なレコードの１つを参照することはできません。 この分離レベルで実行するトランザクションによりビジネスルールを強制しようとすることは、競合するトランザクションをブロックするために注意深く明示的なロックを持たないと、正確に動作しないことが多くあります。

> 注意: PostgreSQL version 9.1より前まででは、シリアライザブル分離レベルの要求はここで説明した通りの動作をそのまま提供していました。 以前のシリアライザブルの動作を維持するためには、リピータブルリードを要求しなければならなくなりました。

## 13.2.3. シリアライザブル分離レベル

シリアライザブル分離レベルは、最も厳しいトランザクションの分離性を提供します。 このレベルではトランザクションが同時にではなく、次から次へと、あたかも順に実行されているように逐次的なトランザクションの実行をエミュレートします。 しかし、このレベルを使ったアプリケーションでは、リピータブルリードレベルと同様に、直列化の失敗によるトランザクションの再実行に備えておく必要があります。 実際、この分離レベルは、（ある時点で）逐次実行可能なすべてのトランザクションにおいて、シリアライザブルトランザクションの同時実行の組が一貫性のないような振る舞いをしていないか監視することを除き、リピータブルリードと全く同じ動きをします。 この監視では、リピータブルリードが示すものを越えてブロックすることはありませんが、監視によりいくらかのオーバーヘッドがあり、直列化異常を引き起こすような状態の検知は、直列化の失敗を引き起こすでしょう。

例えば、以下の初期データを持つ`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の述部ロックは、他のほとんどのデータベースシステムと同様、トランザクションによって実際にアクセスされたデータを元にしています。 これらは、`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を増加することによりインデックススキャンの使用を促進することは有用かもしれません。 問い合わせ実行時間の全体的な変化に不利となる、トランザクションのロールバックや再起動を減少させるように、必ず検討してください。

警告
シリアライザブルトランザクション分離レベルのサポートは、まだホットスタンバイレプリケーションは対象に(項25.5で述べます)加えられていません。 ホットスタンバイモードで現在サポートされた最も厳しい分離レベルはリピータブルリードです。 マスタのシリアライザブルトランザクション中にデータベースへ行なっているすべての永続的な書き込みの間、スタンバイはすべて最終的に一貫した状態に達するだろうということを保証するでしょう。 スタンバイ上で実行されたリピータブルリードトランザクションは、時々マスタのシリアライザブルトランザクションの任意の連続する実行と一致しない過渡状態を見ることができます。

前のページ | ホーム | 次のページ
序文 | 上に戻る | 明示的ロック