Houndを通じてかれこれ1年以上、RDB(PostgreSQL)とORM(Cayenne)につきあってきた。そろそろ、この世界の味がわかってきたので、書きとめておく。
結論:DELETEは深遠な哲学的問題だ。
本題に入る前に、RDBの濫用について片付けておこう。
RDBは、あらゆるコンピュータ技術のなかで、もっとも濫用されている。積もりに積もった装飾をはぎとってみれば、RDBというシロモノは、ある面で比類なく優れているかわりに、それ以外の面では恐ろしく融通がきかない。オブジェクト指向がミニバンだとしたら、RDBはF1マシンだ。装飾に隠されてはいるが、本質は変えられない。このことを忘れて設計した人々は、あとで莫大な額のツケを請求される(こうしてOracleが儲かるわけだ)。
手始めに、行ロックを退けよう。トランザクションを開始するときには、トランザクションを終了するまでの全SQL文を用意しておかなければならない。アプリケーションがトランザクション中にクエリの結果を見て処理するのは、トランザクションの濫用だ。この濫用パターンを仮に、『見る前に跳べ』アンチパターンと名づけよう。
いま永続化にRDBを使う理由はなんだろうか。私見ではそれはスケーラビリティに尽きる。ロックとクエリとデータサイズがスケールアウトすること、これがRDBの魂だ。商用RDBMSのロックマネージャは、いま利用できる実装のなかでは、もっともよくスケールアウトするロックマネージャだ。なのに、『見る前に跳べ』アンチパターンは、ロックマネージャのキャパシティを浪費する。RDBを使うべき理由自体を掘り崩しているわけだ。しかもロックマネージャのキャパシティは見えにくい。その結果、「ワカったときにはもう遅い」という状態に陥る。最近ではココログがこの罠にはまった。
もうひとつ、分散ファイルシステムの代用としてRDBを使うという悪習を退けよう。このパターンにはすでにぴったりの名前がある。『ゴールデンハンマー』アンチパターンだ。噂によると、この世には、画像までRDBに入れる馬鹿がいるらしい。
粗粒度のデータはRDBに入れるべきではない。あまりにもわかりきったことを説明しているようだが、これを説明しなければならないのが現実らしい。分散ファイルシステムを設置するまでもないくらいデータが小さいのなら、普通のファイルシステムを使えばいい。(ただ、Linuxではこれをやると稼動率が下がるかもしれない。ファイルシステムが腐っているのだ。FreeBSD等をお勧めする)
『見る前に跳べ』アンチパターンを退けたので、見てから跳ぶことになる。それには、「アプリケーショントランザクション」(以下「APPトランザクション」と略す)という概念が必要になる。
この概念は『Hibernate イン アクション』(7andy)で詳しく説明されている。RDBのトランザクション単位の外で、必要なレベルの一貫性を確保しながらデータを操作することだと考えればいい。不変であることが求められる行には楽観ロックをかける。不変性が必要なければUnrepeatable readを覚悟する。トランザクションの隔離というあの迷宮が、ここにも広がっているわけだ。隔離レベルはただひとつ、Read committedである。
APPトランザクション中では、必要なデータをRDBから得るために、複数のRDBトランザクションを必要とすることがある。たとえばこんな状況だ。
テーブルA・B・Cがある。テーブルB・CはAを参照している。テーブルAには楽観ロックをかけるが、B・Cには不変性は必要ない。
テーブルCはAに対して非常に大きな多重度を持つ。このため、単純にテーブルAの外部キーでSELECTしただけでは、結果のデータが多すぎる。テーブルAの外部キーだけでなく、さまざまな条件で絞り込む必要がある。その条件を得るには、テーブルBのデータを処理し、さらに外部からデータXを得る必要がある。データXは時間につれて変化するため、あらかじめ処理結果をキャッシュしておくことはできない。
そのため、アプリケーションは最初に、
RDBトランザクション1:テーブルAとBをJOINしてクエリをかける。
これで得られたデータを処理する。その結果を生かして、
RDBトランザクション2:テーブルCにクエリをかける。
さて、RDBトランザクション1と2のあいだで、テーブルB・Cの内容が変更された場合、なにが起こるだろう。UPDATEとINSERTは問題にならない。だが、テーブルCにDELETEがかかっていて、それがRDBトランザクション2で得られるはずの行を消していたら? 楽観ロックをかけたわけではないのに、楽観ロックに失敗したのと同じことになる。不変性は必要なくても、行の存在は必要、というケースがこの世にはある。並行性の高いアプリケーションを作ろうとしたら、必ず遭遇する(Houndがこれだ)。
この問題は、ごく些細なことに見えるが、根が深い。
当面の解決策はある。たとえば以下のとおりだ。
・APPトランザクションの開始時刻をアプリケーション内で保持しておく
・テーブルCに削除時刻のカラムを加え、DELETEのかわりにこれを使う
・RDBトランザクション2のクエリに、APPトランザクション開始時刻の条件を加え、開始後に削除されたものは見えるようにする
・APPトランザクションの生存期間より古い削除時刻の行を定期的にDELETE
これで望みどおりの挙動が得られる。
だが、この解決策は、根本的にダメだ。
まず、テーブルCにかかわるすべてのクエリに、自明性の乏しい条件を加えなければならない。保守のコストはおそらく数倍になるだろう。テストで問題を検出することの難しさを思えば、数倍ではすまないかもしれない。
古い行を定期的にDELETEする、という動作は、追記型RDBMSのVACUUMを連想させる。目的はそれぞれ異なるが、似た作業をやっていることは確かだ。なにかしら間違った枠組みで物事を捉えているのではないか、と考えさせられる。
では、RDBMS側でこのような挙動をサポートする、というのはどうか?
・あらかじめAPPトランザクションの生存期間を設定しておく
・DELETEの際には記憶領域は初期化せず、削除時刻を隠しカラムに記録しておく
・RDBトランザクションを開始するときにAPPトランザクション開始時刻を渡す
・APPトランザクション開始時刻後にDELETEされた行は存在するものとして扱う
・VACUUMの際、APPトランザクションの生存期間を過ぎていない行の記憶領域は初期化しない
これでは足りない。同一APPトランザクション中で、以前のRDBトランザクションでDELETEしたはずの行が読めてしまう。
だがこれを防ぐには、RDBMSがAPPトランザクション自体をサポートせねばならず、それは結局『見る前に跳べ』と同じことになってしまう。
問題をまとめてみよう。
・行の存在は、UPDATEやINSERTでは破壊されないが、DELETEでは破壊される
・そのためDELETEは、APPトランザクションに対して、不要な不変性を押し付ける
「不要な不変性を押し付ける」という性質は、DELETEにだけ存在する。DELETEは深遠な哲学的問題なのだ。
この問題を根本的に解決するには、通常のRDBのデータモデルをやめ、参照とガベージコレクションを採用するしかない。それはDELETEのない世界だ。
アプリケーションと密に結合しなくても、ガベージコレクションは可能だ。データベース上で参照されなくなってもAPPトランザクションの生存期間が過ぎていない行は削除しない、というだけのことだ。
現在のRDBMSの実装上でも、参照とガベージコレクションの真似をすることはできる。最初の解決策(削除時刻のカラムを追加する)をもう少し複雑にすればいい。保守不可能なこと請け合いだが。
歴史をみると、どうやらOODBは必要とされなかったらしい。だが、DELETEのないRDBは必要だ。現在、マルチコアCPUと分散処理が普及しつつある。並行性の高いアプリケーションはこれから桁違いに増えるだろう。
私は挑戦者を待っている。金銭で報いることはどうやらできそうにないが、理解することはできるだろう。