アノマリー問答: ファントムリードとは何か

ファントムリードが何かわからなくなった雑魚(ぼく)が識者に導かれて学んだことを残す。学んだこととしては以下の通り。

  • ファントムリード自体はスナップショット分離により発生しなくなるが、ファントムの発生はスナップショット分離のみで抑止できない
  • 読み取りスキューでは別トランザクションの更新のみをスコープとし、ファントムは考慮していない
  • 書き込みスキューでも同様に更新についてのみ扱うのが伝統的だが、ファントムによって生じた書き込み時のスキューも一緒に説明されるケースがある
    • しかしこのケースをファントムリードとは呼ばない
  • ファントムの発生を抑止する手法のひとつとしてインデックス範囲ロック(ネクストキーロック)がある
  • ANSI SQLにスナップショット分離相当の定義はなく、SERIALIZABLEですら甘い定義になっていることに注意

背景

「ファントムリード」について調べると以下のような記述に出会う。

  • ファントムとは「あるトランザクションでの書き込みが他のトランザクション中の検索クエリの結果を変化させてしまう」効果のこと1
  • ファントムリードとは、あるトランザクションで読み出されるレコード群に含まれるレコードが別のトランザクションによって追加あるいは削除されることでトランザクション中の読み取り結果が変動すること2
  • REPEATABLE READでは防げない
  • SERIALIZABLEでないと防げない
  • MySQL3ではREPEATABLE READでもファントムリードは生じない
    • その根拠として「ギャップロックやネクストキーロックがあるため」と説明しているケースもちらほら見かける

加えて、「書き込みスキュー」について調べると以下のような記述にも遭遇する。

  • 書き込みスキューはスナップショット分離では回避できない
  • SERIALIZABLEでないと防げない

これらについて、特にMySQLの仕様に関連して次のような疑問を抱いた。

  • MySQLのREPEATABLE READ実装は実質的にスナップショット分離であるためファントムリードは起こらないのでは?
  • ギャップロックやネクストキーロックで防いでいるアノマリーは書き込みスキューの一部では?
  • 書き込みスキュー中の読み取り結果の妥当性が別トランザクションによるレコード追加または削除によって失われることもファントムリードと呼んでいるのか?

ログ

雑魚:

世にいうファントムリードってほぼほぼ読み取りスキューか書き込みスキューのことを説明しているように思えるのですが、その感覚はまちがってます?

イノシシ本、「ファントム」の説明は明確に書いてるのに、「ファントムリード」のとこちょろっとほぼファントムと同じ説明が書いてあるだけだったし、実はファントムリードっていう言葉自体さほど重要でも有用でもないのかな、と

識者1:

あー

read/writeのスキューに関してはread/writeのロックをガチガチに取ればいくらでも回避可能なんですけれども

ファントムに関して言うと「存在しなかったものが存在するようになる」っていう新規出現の話をしてしまっていて、存在しないレコードに対する読み書きの話を急にRDBの方向から投げ込まれるので由緒正しいトランザクションの文脈では異質なもの扱いみたいな感じになります。

つまり、read/writeロックだけでなく、ギャップロックとかプレディケートロックみたいな、RDBの文脈に沿った防護が要りますよねっていう話で

確かにそれはRDBとして重要だよねと思う一方で、write skew/read skewとは別の平野の話です。

雑魚:

そのへん曖昧で、少なくともイノシシ本の書き込みスキューの説明だと存在しない行をINSERTするケースとかも紹介されてるんすよね。本来それらはスコープ外なのかな

「countして10行未満ならINSERTする」みたいなのが並行して11行とか入っちゃうようなケースは本来書き込みスキューとは呼ばない?

識者1:

存在しない行をinsertする事に関わるものをwrite skewって呼ぶのは僕の学んだwrite skewの定義とは違う気がします。

というかトランザクションの研究ってW1(x)R1(y)W2(x)…. みたいな記法で表すようなモデル(ページモデルっていうんだけどRDBMSのページとはまた別でまたややこしい)を前提としていて、このモデルには「新規挿入」という概念がありません

識者2:

イノシシ本みてみたけど、サマリにまとまってる書き方が分かりやすいんじゃない

7章の

識者2:

Write Skew は読み取った値に基づいて書き込みを行う場合に起こるもので、 Phantom Read は書き込みが他のトランザクションの読み込み結果に影響を与えるものだから、結果として Write Skew の中における読み取りと関連するよねって話

Write Skew を防ぎたい場合、そもそも Phantom が悪さしてくることがあるから、その対策も必要よねとだけ書いてある

雑魚:

そこでいう「ファントム」の定義がちょっと広そうなんですよね。

「存在しなかったものが存在するようになる」のようなケースだけじゃなく、「読み取ったレコードの別トランザクションによる変化」まで含んだ定義になってるのが違和感の原因っぽい

あるトランザクションでの書き込みが他のトランザクション中の検索クエリの結果を変化させてしまうこの効果はファントムと呼ばれます

とあるんですよね。

write skewに繋がる読み取りの問題のうち、特にレコードの追加・削除による問題をファントムリードと呼んでいそう、ぐらいの理解でいっか

識者2:

いや、それは明確に別

その包含関係ではないよ

ファントムリードは、単体の事象として認識したほうがよい

雑魚:

うーん、そうなのか。。。となるともうRead skewとの違いがもうわからず。。

識者2:

この本の例は、たんにアノマリーが連鎖してるだけ

Read Skew は Repeatable Read ができないだけでも発生する

Phantom Read については、結構古い概念なので、一旦 ANSI の Isolation Level の概念で一旦理解すればいいじゃないかな。

例えばあるテーブルを、定期的にひたすらフルスキャンしてる読み取りトランザクションがあって その中の既存レコードの値が途中で書き換わって見えてしまう話と

存在しないレコードが追加されそれが見えてしまう話は 別で扱われてるってのがそもそも分かりづらいところ

雑魚:

あーーそこがごっちゃになってたのかぁ

識者2:

俺はじめて学んだとき、 「えっ、ファントムだってリピータブルリードって言えそうじゃん?何が違うの?」 って思ったよ

雑魚:

そうとしか思えなかった

識者2:

例えばこんなテーブルに以下のクエリをなげたら id = 2 だけ拾えるよね

SELECT * FROM tab WHERE num < 0

|id|num|
|--+---|
| 1| 10|
| 2| -5|
| 3|  2|

じゃあ id=2 にロックをとればいいよねってなるけど、それだけだと

|id|num|
|--+---|
| 1| 10|
| 2| -5|
| 3|  2|
| 4|-10|

こういうふうに新たに追加されて条件に合致するやつは防げない

この新たに現れたレコードがファントム

これで条件が変わってしまって間違った値を読んでしまうことをファントムリード

雑魚:

はい、典型的なギャップロックで防げるパターンで、よくMySQLがドヤってくる話ですね。

識者2:

MySQL はギャップロックでやりやがるのでロック範囲がはみ出してて[censored]

雑魚:

間違った値を読んでしまうことをファントムリード

ファントムで追加されたレコード読むこと、ではないのがまた伝わりにくい

あとよくMySQLのRRではファントムリード防げるって言われますが

「countして10行未満ならINSERTする」みたいなのが並行して11行とか入っちゃうようなケース

みたいなケースは明示的にテーブルロックとか取らない限り防げないですよね。

インデックスが効かないような条件ではそもそもインデックス範囲ロック(ネクストキーロック)も効かないから保護対象外、という整理なだけかな。MySQL的に。 (edited)

識者2:

ファントムで追加されたレコード読むこと、ではないのがまた伝わりにくい

ごめん書き方が悪かったけど、追加されたレコードを読んでしまうことだよ

ANSI の Isolation Level の定義と、RDBMS の実装上の呼び方が一致してないので、これは MySQL でそうなってしまってるだけなので、MySQL ではそういうもんだと思うしかない。

雑魚:

むむ、なるほど。。。特定のRDBMSの実装に理解が引っ張られがち。。。

識者2:

単純に一度読んだデータは、何度おなじスキャンを繰り返しても同じデータがかえせるか?って話をするとき さっき俺が貼った例で、途中で id=4 が追加される場合 あるトランザクションが同一トランザクション内で、ずっと WHERE < 0 の条件でスキャンを繰り返しててて 同一トランザクションだから最初読んだ結果と同じものが帰り続けてほしいのに ファントムリードが発生してしまう実装でそれをやると 途中から id=2 だけじゃなくて id=4 も帰ってきちゃうよね

おかしいよね

このなかった値を読んでしまうのがファントムリード

Snapshot Isolation をやってる実装だと、トランザクションでの読み取りは、スナップショットにたいして行うから、あとから誰かが更新しても、スナップショットのほうは影響受けないので、更新だろうが追加だろうが影響をうけない

雑魚:

完全理解。「スナップショット分離実装するときにレコードの追加だけ取りこぼすようなことしないやろ」みたいな先入観でそこをごっちゃにしちゃてた

識者2:

俺が最近よく説明につかってるのは

「Google Spreadsheet でデータを管理してて、あなたはその特定の列を集計したいんだけど、周りで他の人がガンガンシートのセルの値更新したり、追加したりしてる環境想像してみて」

「集計しづらいから、自分用にシートコピーしたら、周りの人に邪魔されずに集計できるよね、これが Snapshot Isolation」

雑魚:

わかりやすい

識者2:

もともとこの辺の概念は、1枚のシートをみんなで突っついてるような状態の話(Single Version)であって、シート複製できる(Multi Version)を前提で考えると、なんでそんな事する必要あるん?みたいな感じになっちゃう

Snapshot Isolation はあとから出てきた

雑魚:

そうそう、そのスナップショット分離があとから出てきたっていう文脈にかかわると思ってるのですが

「ファントムリードはRepeatable Readでは防げない」とか「ファントムリードはSerializableでないと防げない」みたいな記述をしばしば見かけるのも、

スナップショット分離があとから出てきたためですかね?スナップショット分離では防げる、と考えていいのですよね?

識者2:

それはあくまで ANSI の Isolation Level の定義の話でしかないので

「ANSIでいうところ」ってことか

識者2:

単に定義と実装がずれてるってだけ

なので俺は ANSI の Isolation Level で語っても、今の実装にそぐわないので、あれで会話するのあんま好きじゃない

雑魚:

実装ばらばらですもんね。特にRRとSerializable。もうこの区分け害悪でしか無いな。。。

識者2:

そうだよ

なので防げる、防ぎたい、防げない Anomaly ベースで会話するほうがいい

が、そうすると更に難しくなるので、人類はもうだめです

もう全部 Serializable にすればいいんだよ

雑魚:

DB売る側も数個のカテゴリや基準を満たしてる・満たしてないで語りたいだろうしな。。。CAP定理とかと一緒っぽい。

識者2:

そしたらどの Anomaly がーとか気にすることが一切なくなる

それをやってるのが Spanner

ただ Serializable といっても例えば Oracle の Serializable、エセ Serializable なので Write Skew がおこる。 (edited)

ただ ANSI の定義では、ファントムリードまで防げれば Serializable を名乗っていいことになってるので

:oracle: < えっ、だってファントムしませんよ?だから Serializable でしょ?うぇい

:oracle: < えっ、Write Skew?なんすかそれ?ANSI の定義にはそんなの書いてないでしょ?

雑魚:

実際はスナップショット分離相当か、OracleのSerializable

識者2:

そう

SSI じゃなくただの SI

(最新バージョンでもそうなのかはしらん)

雑魚:

MySQLのRRも更新のロスト起こしうる、とかもある。

識者2:

なので本気で気にするなら、アノマリー一覧で、どれが起こる起こらないのマトリクス買いて理解したほうがいいよ