非正規化の判断
わざと冗長にする
ここまで冗長を潰してきました。今回はその逆、わざと冗長にする判断を扱います。これを非正規化と呼びます。
注文一覧の画面で合計金額を出したいとします。正規化された設計では、毎回こう計算します。
SELECT sum(quantity * unit_price) FROM order_items WHERE order_id = 1;明細が数件なら一瞬です。しかし注文が1000万件あって、一覧に100件並べるたびにこの計算をすると、じわじわ効いてきます。
そこで orders に total_amount という列を足して、合計を書き込んでおく。これが非正規化です。合計は明細から計算できるので、同じ事実が2か所にある状態を意図して作っています。
何を失うか
失うものははっきりしています。ずれます。
明細を1行追加したのに total_amount を更新し忘れたら、その瞬間から注文一覧の金額は嘘になります。しかも誰も気づきません。エラーは出ないし、画面は普通に表示されるからです。
正規化された設計では、この事故は起こりようがありません。合計は常に明細から計算されるので、明細を足せば合計も変わります。冗長を消すというのは、ずれる余地を消すということです。
それでもやるとき
非正規化は禁じ手ではありません。次の条件が揃ったときの正当な手段です。
- 速度の問題が実測で確認できている — 遅くなりそう、ではなく、実際に測って遅い
- ずれを防ぐ仕掛けがある — 明細を触ったら合計も必ず更新される仕組みを、トリガーやアプリの決まった1か所で作る
- ずれを検出できる — 定期的に再計算して突き合わせ、ずれたら気づけるようにする
この3つが無いまま「速そうだから」で足した集計列は、いずれ必ずずれます。
順番を間違えない
大事なのは順番です。まず正規化し切ってから、測って、必要なら崩す。
最初から崩した設計を作ると、どこが意図的な冗長でどこが設計ミスなのか、あとから誰にも区別できなくなります。正規形という基準があるからこそ、そこから外れた部分を「意図的に外した」と説明できます。
第5章ではインデックスを扱います。速度の問題の多くは、非正規化ではなくインデックスで解けるので、崩すのは最後の手段です。
手を動かす
集計列を足してから明細を変え、何も言われないままずれることを確かめます。
テーブル構造
CREATE TABLE orders (
id integer PRIMARY KEY
);
CREATE TABLE order_items (
order_id integer NOT NULL REFERENCES orders (id),
product_id integer NOT NULL,
quantity integer NOT NULL,
unit_price integer NOT NULL,
PRIMARY KEY (order_id, product_id)
);
INSERT INTO orders VALUES (1), (2), (3);
INSERT INTO order_items VALUES
(1, 1, 1, 128000),
(1, 2, 2, 3200),
(2, 2, 1, 3200),
(2, 3, 1, 8900),
(3, 1, 2, 128000);
期待される出力
| order_id | stored_total | real_total |
|---|---|---|
| 1 | 134400 | 144000 |
| 2 | 12100 | 12100 |
| 3 | 256000 | 256000 |