ファントムリードは起こらないのにSEIRIALIZABLEでない、それがポスグレのREPEATABLE READ

トランザクション分離レベル、苦手意識を拭えない方はわりといらっしゃるんじゃないかと思います。なにしろ私がそうですからね！　だから本題の前にまずはおさらいから入りましょう。

トランザクション
---
SQLの**すべて成功する**か**最初からなかったことになる**かしかないように管理された一連の操作のことをトランザクションと呼びます。

Javaの`synchronized`ブロックのようなものをイメージしてしまうこともあるかもしれませんが、それは全然違いまして、トランザクションの進行中にもほかのセッションによってDB内のデータは刻々と書き換えられていきます。**最初からなかったことにできる**、それだけがトランザクションの特徴ですから。

トランザクション分離
---
しかし進行中にどんどんデータが変化していってしまうのではプログラミングになりません。ある程度、ほかのセッションからの干渉を防いでもらわないと困ります。それがトランザクションの分離です。
トランザクションの分離は、具体的には**進行中にどんなデータ変化を見てしまわないことを保証してもらうか**で表されます。

そしてこの「どんなデータ変化が」には、SQL標準で2種類しか定義されていません。

- レコード単位のデータ再現保証
- テーブル単位のデータ再現保証

この二つです。
テーブル単位の方はレコード単位の方を含んでいますから、

- データ再現保証なし
- レコード単位のデータ再現保証のみ
- レコード単位＋テーブル単位のデータ再現保証を両方

の3段階の分離レベルがありえるとなります。そこで出てきます、あのイヤな表。

||レコード単位のデータ再現保証|テーブル単位のデータ再現保証|
|---|---|---|
|READ COMMITTED|**×非保証**<br>一度読んだレコードを再度読んだら変化しているかもしれない<br>**非再現リード**<br>**（ファジーリード）**|**×非保証**<br>一度問い合わせて見つからなかったレコードが再度問い合わせて見つかるかもしれない<br>**ファントムリード**|
|REPEATABLE READ|**○保証あり**|**×非保証**|
|SERIALIZABLE|**○保証あり**|**○保証あり**|

さてこの表の最下段に注目します。`SERIALIZABLE`とあります。

えっ、「READ UNCOMMITTED」は？　「ダーティリード」はって？　そんな野蛮なものはありません。ないんですそんなものは。

SERIALIZABLE
---
SERIALIZABLE - “直列化可能”とは、各トランザクションをあたかもシングルスレッドで順番に実行したかのように、**実行順序関係が決められる**ことを意味します。

もちろん本当にシングルスレッドでやる必要はありません。並列でやってくれていいんです。っていうか並列でやってくれないと困ります。ただ、並列に処理した結果前後関係が決められなくなるような矛盾が起こったら完了時にそれを検出して確実にエラーにしてくれればいいんです。

さて、テーブル単位でのデータ再現保証があることって、トランザクションの実行順序関係を決定できることとイコールなんでしょうか？　それで合っているような何か間違っているような⋯　このモヤモヤはちょっと保留しておきます。しかし少なくともSQL標準はイコールだと定義づけています。

そろそろこの記事のタイトルがSQLの標準と食い違ったことを言っていること、見えてきたでしょうか。

PostgreSQLのREPEATABLE READ
---
PostgreSQLの9.1以降では、トランザクション分離レベルに`REPEATABLE READ`があります。そして、それはマニュアルによるとファントムリードも起こさないことになっています。

おや、ファントムリードを起こさないならすでにそれは`SERIALIZABLE`レベルでは？　しかし、`SERIALIZABLE`は別にちゃんとあります。何が違う？　そう、`REPEATABLE READ`はファントムリードを起こさないけれど、実行順序関係を決定できなくなるケースがあるというレベルなのです。

それも、そんな特別に複雑なトランザクションでなくてもそれは発生します。例えばこんなトランザクション、書いたことある人は多いんじゃないでしょうか。

```sql
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- ユーザーネーム alice を使用しているユーザーがいないことを確認
SELECT COUNT(*)
FROM users
WHERE username = 'alice';

-- 前の問い合わせ結果が0だったら、ユーザーネーム alice のユーザー新規登録実行
INSERT INTO users (username)
VALUES ('alice');

COMMIT;
```

UNIQUEインデックス張らずにアプリコードで一意性を保とうとしているコードですね。非常によろしくないんですが諸般の事情で書かざるを得ないこと、まれによくあります。
（諸般の事情というのが論理削除とうまく共存できなくて、という話だったら、以前書いた記事ですがお役に立てば: https://qiita.com/yuba/items/70165875cfe02b03513d）

このトランザクション、`REPEATABLE READ`レベル同士の2セッションが並行実行して両方成功してしまいます。'alice'さん、いとも簡単に多重登録されてしまいます。全然ダメじゃん。

`REPEATABLE READ`レベルはファントムリードを防ぐのに何を防げないか。
**計算の根拠となったデータがINSERTによって変わったこと**を検知できません。なるほどそれじゃ多重登録が起こってしまうわけです。そして、「やっぱりファントムリードを防いだだけじゃSERIALIZABLEとは言えないじゃん」が先ほどの疑問の答えとなってきます。

PostgreSQL 9.1で何が起こったか
---
先ほど「9.1以降では」とバージョン番号をちらっと書きました。ポスグレはこの9.1でトランザクション分離レベルを劇的に進化させています。

いまの`REPEATABLE READ`レベル、9.0以前では実は`SERIALIZABLE`レベルと呼んでいました。上記のとおり直列化可能とは言えない仕様なのですが、マニュアルにも「だって仕方ない、まともに防ごうとしたら大変すぎるし⋯」的に注意書きしてある状態でした。

それが9.1で一転して、真の`SERIALIZABLE`を実現してしまったのです。それは**述語ロック**という機構が導入されてのことでした。「述語」とは、先ほどのトランザクション例で言うと最初のSELECTの`WHERE username = 'alice'`のことです。テーブルに対して
**「`username = 'alice'`って条件で読み取っていった奴がいるから、当てはまるレコードをINSERTするときは注意しろよ」**
という印を付けられるようになったということです。9.0のマニュアルでは「とてもそこまでできない」と書いてあったのに。すごい。勢いしか感じない。

9.1から登場した`REPEATABLE READ`は、9.0以前の不完全な`SERIALIZABLE`を互換のために改称して残したものとなります。

まとめ
---

- ファントムリードを防いだだけではSERIALIZABLEの十分条件ではない。
- SERIALIZABLEレベルは思っている以上に必要。「存在しなければ挿入」の処理がすでにSERIALIZABLEを必要としている。
- SERIALIZABLEレベルは、コミット時に実行順序関係の矛盾を検知してエラーになる可能性がある。アプリコードにはリトライ処理が必須。
- PostgreSQLの`SERIALIZABLE`は9.0以前と9.1以降で意味が違うので、移行の際には注意が必要。