ぱと隊長日誌

ブログ運用もエンジニアとしての生き方も模索中

PostgreSQLマニュアルの「リピータブルリード分離レベル」における「制御レコード」とはなにか?

PostgreSQL(9.1以降)マニュアルの「13.2.2. リピータブルリード分離レベル」に以下の記述があります。

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

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

説明に「制御レコード」とありますが、その意味をこの記述から理解することは困難です。実際、PostgreSQLのMLにも同様の質問が寄せられており、その回答を以下のリンクで見ることができます。
Re: Confused by example in 13.2.2

回答ではドキュメントを理解するため、PostgreSQLWikiを紹介しています。
SSI - PostgreSQL wiki

この例では銀行の小切手処理を例に挙げています。ここではcontrolテーブルのdeposit_no列で処理対象のバッチ(小切手の集合)を制御しています。例えば、4/1に受領した小切手はdeposit_noは1、4/2なら2、といった具合です。受領した小切手の詳細はこのdeposit_noの値と共にreceiptテーブルに格納します。

Wikiの記載をベースに問題となるケースを説明します。

  1. 4/1の営業終了時刻間際に銀行の担当者が小切手の処理を行いました。トランザクションT1にてdeposit_no=1で小切手の情報をreceiptテーブルにinsertします。ですが、処理が遅れたため、この時点ではまだcommitしていません。
  2. 銀行の上役が営業終了時刻ちょうどに小切手の締め処理を行いました。これにより、トランザクションT2でdeposit_noは1→2へインクリメントされました。
  3. 小切手の締め処理が行われたことで、後続の小切手一覧出力処理が行われます。トランザクションT3でdeposit_no=1の小切手一覧を出力しますが、T1はcommitしていないため、この一覧に含まれません。
  4. ようやくT1がcommitされました。

本来期待していたトランザクションの依存関係はT1→T2→T3でした。ですが、T1のcommit前にT2, T3が行われたことで、T3→T1の依存が生まれました(この順でなければT3にT1の結果が含まれないことを説明できない)。となると、T1→T2→T3→T1という矛盾した依存が発生します。
また、一見すると奇妙なことですが、T1のcommit前にT3の Read Only なトランザクションが発生しなければ、上記の矛盾は生じません。この場合、期待していた結果通りとなります。

これは"Read Only Anomaly"と呼ばれているものです。以下の解説記事が分かりやすいです。
いろんなAnomaly - Qiita

冒頭のPostgreSQLマニュアルで「制御レコード」とはWikiの例でいえばcontrolテーブルのレコードであり、「詳細なレコード」とはreceiptテーブルのレコードのことになります。