RDBの素晴らしさに気づき始めたという話
リレーショナル・データベースの設計について思ったことをうだうだと書いてみます。
- 「そんな基本的なことを……」
- 「しかも、間違ってるし」
とかいろいろあると思うので、指摘していただけると嬉しいです。
そもそもの問題
僕がDBを触り始めたのはRailsでWebアプリを書き始めたときです。 そのため、長い間RailsのMigrationやActiveRecordを通してRDBを扱ってきました*1。
Rails入門書では、scaffoldやmigrationに関しては解説してくれますが、DBそのものの扱い方に関しては触れられていませんでした*2。
RDBに関する基本的な知識もないままRailsを使った結果、僕はRDBを単なるデータを保存しておくだけの場所だと思ってしまいました。 そのため、FOREIGN KEY制約やUNIQUE制約を使ったことがありませんでした*3*4。
RDBは単なる永続化層ではない
RDBに限らず、DBにとって永続化が重要な性質のひとつであるのは間違いないと思います。
しかし、それだけであればファイルを使っても問題ないはずです。 ファイルを使って単純に永続化するとパフォーマンス上の問題が発生する気がしますが、それは実装次第でどうにかできると思うので本質的な問題ではないと思います。
では、RDBとは何なのか?
結論から言えば、データの整合性を保つための砦だと考えています。
今まで経験した開発が「個人や少人数で小規模のアプリケーションを新規開発する」というものしかなかったため、全然意識していなかったのですが、データベースが利用される文脈としては、
- 既存のシステムのDBに新しいアプリケーションが接続する
- アプリケーションを介さず直接SQLを実行する
のようなケースも想定しておかなければならないため、Model層にValidationを追加するだけでは不十分です。
例えば、アプリケーションのModel層にUNIQUE制約をかけた場合でも、Model層でUNIQUE制約をかけ損なったアプリケーションからのレコード追加が行われた場合やINSERT文を直接実行した場合に、重複したレコードが登録されてしまいます。
制約
先述の通り、アプリケーション層(Model層)でのValidationでは、データの整合性を保証できません。
そこで、データに最も近いところにあるRDBMSで制約をかけ、データの参照はRDBMSを介して行う*5ことによって、以下の様な種類の整合性を強制します。
- PRIMARY KEY
- NOT NULL
- UNIQUE
- FOREIGN KEY
- CHECK
いかなるアプリケーションやSQLも、RDBMSによって強制される制約に違反するレコードを追加することはできません*6。
正規化
正規化を行う目的はいくつかあると思いますが、データの整合性という点では「One Fact in One Place」が目的です。 あるデータが複数の箇所に散在していると、そのデータに変更が生じた時にその全てを変更せねばならず漏れが生じる恐れがあるため、それを防ぐためということですね。
制約に関しては、無自覚にRailsを使った結果Validationでゴリ押しするアプリケーションを書いていましたが、正規化に関しては普通にRailsを使っていてもある程度実現できていました。
この理由としては、
- マスタという概念を持っていた
- 1対多の関連を実現するためにはテーブルを分割する必要がある
- そもそも1対多の関連が必要とされるものには別の名前がつくことが多い
- ActiveRecordパターンではクラスを分けるとテーブルも分かれる
- Railsチュートリアルで「article has many comments」型の例が頻出する
などがあると思います。
制約と正規化の差
少なくとも僕に関してのみ言えば、正規化は特に意識しなくてもある程度できていたのに、制約に関してはあまり出来ていませんでした。 この差はわりと大きいと感じていて、レールの敷き方が甘いんじゃないかと思います。
Railsで1対多の関連を実現する際にはチュートリアルの通り、素直に進めていけば最低限キレイなテーブルが出来上がると思います*7。
一方、制約はチュートリアルなどで触れられる機会が少なく、Railsの規約で生成されるid列のPRIMARY KEY制約しかついていないDBも多いと思います。
そもそも、Migrationで付与できる制約がNOT NULL制約しかない*8のが問題だし、そのNOT NULL制約に関しても、scaffoldした後にnull: false
を追記してからrake db:migrate
するチュートリアルはあまり見ない気がします。
制約へのレールを敷設する
Railsで制約を扱いやすくするためには、現在のPRIMARY KEYとNOT NULL以外にもMigrationから制約を追加できるようにするべきだと思います*9。
また、せっかくModelでvalidates :hoge, presence: true
とかhas_many :hoges
とかvalidates :hoge, uniqueness: true
とか長々と宣言しているので、そのあたりの情報を元にDBに適切に制約が張られているかどうかをチェックするgemとかがあっても良いと思います*10。
正規化と非正規化
RDBはデータの砦としてデータの整合性を保つ必要があるため、非正規化は極力行うべきではないと考えています。
弱小アプリケーションしか作ったことがないので、パフォーマンスのシビアさが実感としてわからないのですが、そもそもRDBが「リレーショナル・データベース」であって「テーブル・データベース」ではないのには理由があって、リレーション同士を演算してリレーションを返すからこそリレーショナル・データベースだと思います。
そして、リレーショナルデータベース入門―データモデル・SQL・管理システム (Information&Computing)で、
結合演算は先に述べたとおり、リレーションという単位で格納されているデータをつなぎ合わせて、新たな”意味"を発見するのに欠かせない演算であり、リレーショナル代数演算の中で最も大事な演算である。
と書かれているとおりJOINしてこそRDBだと信じているのでRDBは綺麗に正規化し、パフォーマンスなどの問題は
- リードレプリカの設置
- マテリアライズド・ビューの使用
- キャッシュ層の設置
などによって解決したいと考えています。
制約と正規化では実現できないこと
RDBの素晴らしさを知った今、制約と正規化でデータの整合性をガチガチに縛りたいのですが、少なくとも僕が知る限りでは制約と正規化で実現できないことがあります。
- 1:nの関連のnがある数以上であること(n≧k)
- あるレコードのカラム間でのCHECK制約
- Immutableなカラム*11
これらの縛り方をRDBMS側でできると個人的に嬉しいのですが、現在のRDBMSでは実現できなさそうです。 もし出来るようであれば教えて下さい。
さいごに、そして、振り子の原理
以上、まとまりのない文章になってしまいましたが、今思っていることを素直に書いてみました。 今までRDBをないがしろにしてきた反動で、現在の僕はRDB偏重主義に陥っている可能性が高いと思いますので、その辺りも含めてコメント等頂けると嬉しいです*12。
それにしてもブログ書くの下手だなーorz
*1:DMLは多少書いたことがありましたが、DDLは書いたことがありませんでした。
*2:Rails入門書はRailsの入門書であってDBの入門書ではないので、当然です。
*3:FOREIGN KEY制約などを「使わなかった」のではなく、「知らなかった」というのが本当のところです。
*4:NOT NULL制約はmigrationを通してかろうじて使っていましたが、UNIQUE制約はModel層のValidationで実現していました。
*6:厳密にはRDBMSが使用しているファイルを直接書き換えれば可能な気もしますが、あまり現実的ではないかと思われます。
*7:若干粒度が大きかったりするとは思いますが。
*8:executeを使わない場合。Migration型でreferencesを指定してもFOREIGN KEYは張られないようです。
*9:誰でも思いつきそうなことなので、実装されていないのにはそれなりの理由が?
*10:調べてないのでもしあったらごめんなさい。
*11:かなり面倒な権限管理をすれば実現できそうですが。
*12:「パフォーマンスなめんな」とか。