リレーショナルモデル — 表一つに事実一つを語らせよ
一言でいうと
リレーショナルモデルは、データを行と列の表で表現し、行を一意に識別するキーと、表の間の参照を制約として強制することで、データが自らルールを守るようにします。
なぜ必要なのか
リレーショナルモデル以前は、データがファイル構造やポインターで結ばれており、検索方法が保存構造に縛られていました。保存方式を変えると、プログラムをすべて直す必要がありました。
リレーショナルモデルの決定的なアイデアは、何が欲しいかだけを書き、どう探すかはデータベースに決めさせようというものです。SQLが宣言的である理由がここにあります。私たちがWHERE status = 'paid'とだけ書けば、インデックスを使うか全体を走査するかは、オプティマイザーがそのときの統計を見て決めます。
どう動くのか
用語から整理すると、表はリレーション、行はタプル、列は属性です。ここに3種類のキーが加わります。
- 主キー: 行を一意に識別します。NULLにはなれません。
- 候補キー: 主キーになりえた、他の一意な属性です。代理キーを使う場合でも、自然キーがあるなら、一意制約として必ず残す必要があります。
- 外部キー: 他の表のキーを参照します。参照整合性をデータベースが強制します。
NULLは、リレーショナルモデルで最も誤解されている部分です。NULLは「値がない」ではなく、「値がわからない」に近いものです。そのため、NULL = NULLは真ではなく不明(unknown)で、WHERE email = NULLはどの行も返しません。IS NULLを使う必要がある理由です。この性質は、あちこちに波及します。count(*)は行を数えますが、count(email)はNULLでない値だけを数えます。NOT INのリストにNULLが1つでもあると、結果がすべて消えます。
制約をデータベースに置く理由も、押さえておく価値があります。「この値は常に0より大きい」というルールをアプリケーションだけに置くと、3つの経路で破られます。運用中の手動SQL、バッチスクリプト、後から加わった別のサービスです。制約は、この3つをすべて防ぎます。
現場での姿
型の選択は、元に戻すのが最も難しい決定です。PostgreSQLでカラムの型を変更するのは、たいていテーブル全体の書き直しで、その間、強いロックがかかります。一方、インデックスはいつでも追加でき、削除できます。そのため、型とキーには時間をかけ、インデックスは実際のクエリパターンを見てから後で決めるという順序が合理的です。
よく間違える2つだけ挙げておきます。金額に浮動小数点を使うと、合計が1ウォンずつずれます。正確な計算が必要なら、numericを使います。そして、時刻のカラムに何も考えずにtimestampと書くと、タイムゾーンなしの型になります。ある瞬間を指す値にはtimestamptzを使う必要があり、2つの型の保存サイズは8バイトで同じなので、容量を節約するためにタイムゾーンなしの型を選ぶ理由はありません。
正規化はどこまで行うのか
表を分けるルールには名前が付いていますが、実務で覚えるべきものは3段階までで、その3つは実は1文に縮められます。1つの列には値を1つだけ入れ、列はすべて主キー全体に従属していなければならず、キーでない列同士が互いを決定してはいけません。
- 第1正規形: 1つのセルに
"010-1111-2222, 010-3333-4444"のように値を複数入れません。そのように入れると、「この番号を使っている人を探す」が文字列検索になり、インデックスを使えません。 - 第2正規形: 主キーが
(주문번호, 상품번호)の表に商品名を置くと、その値はキーの半分にだけ従属します(列名は、韓国語でそれぞれ注文番号と商品番号を意味します)。同じ商品が注文のたびに繰り返し保存され、名前を直すときに1行でも漏らすと、そのときから同じ商品に名前が2つできます。 - 第3正規形: 注文表に
우편번호と도시を一緒に置くと(列名は、韓国語でそれぞれ郵便番号と都市を意味します)、都市は注文ではなく郵便番号に従属します。郵便番号が変われば都市も一緒に直す必要がありますが、そのルールはどこにも書かれていません。
3つが防ぐものは、結局1つです。同じ事実が複数の場所に書かれると、いつかそのうちの1つだけが直され、そのときからデータベースは、互いに矛盾する2つの話を同時に語ります。
だからといって、やみくもに細かく分けるのが正解ではありません。分けるほど結合が増え、一覧画面1つを描くために、表を5、6個結ぶことになります。そのため、読み取りが圧倒的に多く、その値がほとんど変わらないときに限って、あえて重複を残す選択をします。注文に、その時点の商品名と価格をコピーしておくのが代表的な例です。これは正規化違反ではなく、むしろ正確なモデリングです。注文時点の価格は、商品の現在の価格とは別の事実だからです。
逆に、頻繁に変わる値を性能を理由にコピーしておくと、必ず代償を払います。会員等級を注文表にコピーしておくと、等級が変わるたびに、過去の注文まで一緒に変わるべきかどうかを毎回判断する必要があり、その判断はコードのあちこちに散らばります。判断基準は、次のように立てれば十分です。その値が「今の事実」なら参照し、「そのときの事実」ならコピーします。
次のラボですること
本物のPostgreSQL 16が起動したPodで、Eコマースのスキーマを照会します。特に、メールアドレスがNULLの顧客を探すステップで、= NULLがなぜ通用しないのかを手で確認することになります。