コンピューターサイエンス:アルゴリズム / OS / ネットワーク / DB

非正規化のトレードオフ

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

非正規化のトレードオフ

このレッスンで分かること

  • 非正規化が必要になる典型的な 3 つのパターン
  • 「冗長を許容して何を得るか」の判断基準
  • 非正規化の落とし穴と運用での守り方

非正規化のトレードオフ とは

読み取り性能のために冗長を許容する判断基準を学ぶ。本レッスンでは、非正規化のトレードオフ の基本から実際の使いどころまでを整理し、現場で迷わず使える形に落とし込みます。

非正規化とは

非正規化 (Denormalization) とは、あえて正規形を崩して 冗長性 を持たせる設計手法です。3NF まで分解した後、特定の目的のためにテーブルを統合したり、計算済みの値を保存したりします。

「正規化と逆のことをするなら、最初からやらなければよかったのでは」と思うかもしれません。違います。先に 3NF まで設計し、計測した結果として 非正規化に進むのが正しい順序です。これを 「正規化してから戦略的に崩す」 と呼びます。

非正規化が必要になる 3 つのパターン

diagram (will load when visible)

パターン 1:読み取り頻度が極端に高い

商品一覧ページがアクセス数 1 万 req/sec で、毎回 products JOIN categories JOIN brands を実行するとボトルネックになります。category_namebrand_nameproducts に保存しておけば、JOIN なしで取得できます。

SQL クエリ

CREATE TABLE products ( id INT PRIMARY KEY, name VARCHAR(100), category_id INT, category_name VARCHAR(50), -- 冗長カラム brand_id INT, brand_name VARCHAR(50), -- 冗長カラム FOREIGN KEY (category_id) REFERENCES categories(id) );

カテゴリ名が変わったら 2 箇所更新が必要ですが、カテゴリ名の更新頻度は年に数回なら許容範囲です。

パターン 2:集計クエリが重い

「ユーザーごとの注文合計金額」を毎回 SUM(amount) で計算すると、注文数が増えるほど遅くなります。users.total_amount というカラムを持って、注文追加時に更新する方法があります。

SQL クエリ

ALTER TABLE users ADD COLUMN total_amount BIGINT DEFAULT 0; -- 注文追加時 UPDATE users SET total_amount = total_amount + 500 WHERE id = ?;

これは 集計の事前計算 と呼ばれ、ダッシュボード系で多用されます。

パターン 3:履歴を凍結したい

注文時の商品価格を order_items.unit_price に保存するのは非正規化の一種です。products.price を参照すると価格が変わったときに過去の注文金額まで変わってしまうからです。

SQL クエリ

CREATE TABLE order_items ( order_id INT, product_id INT, quantity INT, unit_price INT, -- 注文時点のスナップショット PRIMARY KEY (order_id, product_id) );

これは 「時間を凍結するための冗長」 で、業務システムでは必須テクニックです。

非正規化の落とし穴

冗長を持つということは、真実が複数箇所に分散 するということです。同期がズレると地獄になります。

  • カテゴリ名を変えたら categoriesproducts の両方を更新したか
  • 注文を追加したら users.total_amount を更新したか
  • 注文を取り消したら集計を減算したか

これらを アプリコードで毎回正しく書く のは現実的ではありません。次のいずれかの守りを入れます。

同期を守る 3 つの仕組み

  1. トリガー — DB 側でレコード変更時に自動で他テーブルを更新
  2. マテリアライズドビュー — PostgreSQL の MATERIALIZED VIEW で定期再構築
  3. イベント駆動 — アプリ側で「注文作成イベント」を発火し、集計テーブルを更新

トリガーは強力ですが、デバッグが難しく、隠れたパフォーマンス問題を生みやすいので慎重に。マテビューは更新タイミングを制御できるのが利点です。

非正規化を入れたら、必ず「同期する仕組み」とセットで設計する。

判断基準

非正規化を採用するか迷ったら、次の質問に答えます。

  • 正規化のままで本当に遅いか(まず計測する
  • 読み取りと書き込みの比率は何対何か
  • 元データの更新頻度は十分低いか
  • 同期失敗時の業務影響はどれくらいか

「速くしたいから」だけで非正規化に進むと、後で必ず不整合事故が起きます。速度問題が定量的に証明できた場合のみ 進みましょう。

OLTP と OLAP

ざっくり言えば、OLTP (オンライントランザクション処理:注文や決済)は正規化、OLAP (オンライン分析処理:レポートやダッシュボード)は非正規化が向きます。OLAP では スタースキーマスノーフレークスキーマ のように、意図的に非正規化したファクトテーブルとディメンションテーブルを使います。

やってみよう

  • 自分のプロジェクトの最も重いクエリを 1 つ EXPLAIN し、ボトルネックの JOIN を特定する
  • そこに非正規化を入れた場合、同期する場所がどこになるか書き出す
  • もしマテリアライズドビューで対処するならどんな SELECT 文か書いてみる

よくある質問

Q. なぜ正規化するのですか?

A. データの重複を排除し、更新時の不整合(更新異常・挿入異常・削除異常)を防ぐためです。第 3 正規形まで適用すると保守性が大きく上がります。一方で JOIN が増えるため、検索性能と保守性のバランスで非正規化することもあります。

Q. 1NF と 2NF の違いは?

A. 1NF は「各セルが単一値」、2NF は「主キーの一部だけに依存するカラムをテーブル分割」する形です。複合キーの片方だけに紐づく属性は別テーブルに切り出すと 2NF になります。1NF を満たさないと 2NF 以降の議論はできません。

Q. 全部正規化すれば最適ですか?

A. 理想ですが、JOIN が増えてパフォーマンスが落ちる場面では非正規化(カラム複製、計算結果のキャッシュ)も有効です。OLTP は 3NF、レポーティング用 DWH はスタースキーマ(非正規化)と、用途で使い分けるのが現代的なアプローチです。

次のレッスン

次は インデックスの役割 で、読み取り性能のために冗長を許容する判断基準を学ぶ を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. 非正規化のトレードオフ の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. 非正規化のトレードオフ とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン