MySQL の非正規化と設計判断
正規化がデータの冗長性を排除する手法であるのに対し、非正規化は意図的に冗長性を持たせる設計判断です。パフォーマンスや運用の要件に応じて、正規化のルールをあえて崩す場面があります。
なぜ非正規化するのか
第3正規形まで正規化すると、データの整合性は高まりますが、複数テーブルの JOIN が増えてクエリが複雑になります。読み取り性能が求められる場面では、JOIN のコストが無視できなくなることがあります。
データの一貫性が保たれ、更新時の不整合リスクが低い。ストレージ効率もよい。
JOIN を減らすことで読み取りクエリが高速化する。1回のクエリで必要なデータをすべて取得できるため、アプリケーションコードもシンプルになる。
非正規化の典型パターン
注文テーブルに合計金額を持たせるパターンです。本来は注文明細の SUM で算出できますが、毎回集計するとコストが高いため、INSERT/UPDATE のタイミングで合計値を保存しておきます。
注文テーブルに注文時点の商品名や価格を保存するパターンです。商品マスタの価格が後から変更されても、注文時点の情報を正確に保持できるという業務上の理由もあります。
ユーザーテーブルにフォロワー数や投稿数を持たせるパターンです。COUNT クエリの代わりにカラム値を読むだけで済むため、高トラフィックな表示処理が劇的に高速化します。
非正規化を判断する基準
非正規化はトレードオフの判断であり、闇雲に行うと保守性が低下します。以下の問いに YES と答えられる場合に検討するとよいでしょう。
正規化された状態でクエリ性能に問題があるか
インデックス最適化だけでは解決できないか
非正規化によるデータ不整合リスクを管理できるか
まずインデックスの見直しやクエリの最適化を試し、それでも足りない場合の最終手段として非正規化を選ぶのが健全な判断プロセスです。
非正規化のリスク管理
非正規化したカラムは、元データと値がずれる可能性を常に抱えています。そのリスクを管理するための仕組みを必ずセットで用意しましょう。
-- カウンターカラムの例
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
post_count INT NOT NULL DEFAULT 0 -- 非正規化カラム
);
-- 投稿時にカウンターを更新するトリガー
DELIMITER //
CREATE TRIGGER after_post_insert
AFTER INSERT ON posts
FOR EACH ROW
BEGIN
UPDATE users SET post_count = post_count + 1
WHERE id = NEW.user_id;
END //
DELIMITER ;トリガーのほかにも、定期バッチで整合性を検証する方法があります。SELECT u.post_count, COUNT(p.id) のように実データと非正規化カラムを突合し、差異があればアラートを上げる仕組みを入れておくと安心です。
非正規化は「やむを得ない妥協」ではなく「意図的な設計判断」です。なぜそのカラムを非正規化したのか、整合性をどう担保するのかを設計ドキュメントに残しておくことで、将来のメンテナンスが格段に楽になります。



