非正規化の判断

わざと冗長にする

ここまで冗長を潰してきました。今回はその逆、わざと冗長にする判断を扱います。これを非正規化と呼びます。

注文一覧の画面で合計金額を出したいとします。正規化された設計では、毎回こう計算します。

SELECT sum(quantity * unit_price) FROM order_items WHERE order_id = 1;

明細が数件なら一瞬です。しかし注文が1000万件あって、一覧に100件並べるたびにこの計算をすると、じわじわ効いてきます。

そこで orderstotal_amount という列を足して、合計を書き込んでおく。これが非正規化です。合計は明細から計算できるので、同じ事実が2か所にある状態を意図して作っています。

何を失うか

失うものははっきりしています。ずれます。

明細を1行追加したのに total_amount を更新し忘れたら、その瞬間から注文一覧の金額は嘘になります。しかも誰も気づきません。エラーは出ないし、画面は普通に表示されるからです。

正規化された設計では、この事故は起こりようがありません。合計は常に明細から計算されるので、明細を足せば合計も変わります。冗長を消すというのは、ずれる余地を消すということです。

それでもやるとき

非正規化は禁じ手ではありません。次の条件が揃ったときの正当な手段です。

  1. 速度の問題が実測で確認できている — 遅くなりそう、ではなく、実際に測って遅い
  2. ずれを防ぐ仕掛けがある — 明細を触ったら合計も必ず更新される仕組みを、トリガーやアプリの決まった1か所で作る
  3. ずれを検出できる — 定期的に再計算して突き合わせ、ずれたら気づけるようにする

この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_idstored_totalreal_total
1134400144000
21210012100
3256000256000

ヒント

query.sql
学習モード
コードの実行結果
データベースを初期化中...