はじめに
正規化を学習すると、「正規化も大事だけど、パフォーマンスのために非正規化することもあるよ」と言われます。ただ、正直あんまり非正規化するタイミングがピンと来なかったんですよね。
「結局、いつ非正規化したらいいねん!!」
この記事は、 「いつ非正規化するのか?」の判断基準を、実体験ベースで整理します。
- 前半: 導入と教訓
- 後半: 教訓をより理解するための具体例
前半の内容がイマイチわからん場合は、後半も並行して読むといいと思います。
まずお前誰やねん!(自己紹介)
本題の前に軽い自己紹介を。
大阪大学工学部 新4回生の 辻 孝弥 と申します。
普段は、4,000人の大学生が使う成績管理サイト 『GPAナビ』 を運営しています。
最近は農業にめちゃくちゃ興味があり、今年の5月頃から3ヶ月〜半年ほど、住み込みで農業に携わりたいと考えています。
「うちに来ていいよ!」「面白い農場を知ってるよ」という方がいれば、ぜひコメントやDMなどで教えてください!(ガチで働き口募集中です🌾)
余談はさておき、、、今回は、とある案件で初めてDB設計を任された際に、 「正規化をガチガチに守りすぎた」 という苦い失敗談をベースにこの記事を執筆しました。
前半:導入と教訓
何が起きたか(失敗の背景)
- プロジェクト: ストレスチェックのシステム
- 規模感: 〜1,000万円
この案件でDB設計を任された私は、チームレビューで「構造が複雑すぎる」という指摘を受けました。そこで「とにかくシンプルにしよう」と思い、ER図の見た目と正規化(特に第3正規形)を強く意識して作り直しました。
(『達人に学ぶDB設計徹底指南書』に「業務では第3正規形まで」という趣旨の記述があった記憶もあり、それが後押しになりました。)
しかし結果は逆でした。正規化を優先しすぎたことで、取得・更新ロジックが複雑化し、変更に弱い設計になってしまいました。
「パフォーマンスと正規化の両立が大事」 って参考書にはよく書かれますが、私はその塩梅がいまいちピンときませんでした。
今ならこう判断する(基準)
まず、私がこの体験談で得た、思考ロジック(大袈裟にいうと秘伝のタレ的なもの)を示します。
一言で言うと:「普段困る方(読み取り or 更新)に合わせて、冗長さを選ぶ」
2. の「実装が複雑」「パフォーマンスが悪い」ってなんやねん!!
具体的に以下のようなサインが出たら注意です。
「実装が複雑」になるサイン
- 画面のたびにJOINが連鎖し、クエリとDTO(返却形)が肥大化する
- 「親を辿らないと絞れない/表示できない」情報が増える(例:部署一覧を出すのに事業所→会社が必要)
- 更新が波及しやすく、変更時に複数テーブルの整合性を同時に守る必要がある
- 例外ケース(所属なし・暫定紐付け・将来の仕様変更)に弱く、分岐が増える
「パフォーマンスが悪くなる」サイン
- JOINが多い前提になり、インデックス設計・チューニングの難易度が上がる
- 検索・一覧など高頻度クエリが複雑になり、データ量が増えるほど効いてくる
- 「最終的に欲しい形」が常に集計・結合後の形で、キャッシュや読み取り最適化がしにくい
結論: ER図の美しさよりも「日々書くコードの単純さ」と「運用で事故らないこと」を優先する。
3. の「更新コストが高い」ってなんやねん!
後半の「トレードオフ」「根本要因:マルチテナントの隔離」の章を見て理解してください。
後半:
具体例(Company > WorkPlace > Department)
抽象論だけだと伝わりづらいので、実際に困ったパターンを例にします。
背景(やりたかったこと)
Company, WorkPlace, DepartmentのDB設計(ER図)を書く!!
【定義】Company, WorkPlace, Departmentとは
Company:会社(株式会社〇〇)WorkPlace:事業場(例:大阪本社/梅田支店/〇〇工場/△△店)Department:部署(例:営業部/開発部)※ 今回、開発するストレスチェックシステムの要件より、
- 大阪本社の営業部
- 名古屋支社の営業部
というように、部署は必ず事業場に紐づくデータであると定義しています。
❌ アンチパターン:ER図はきれい、読み取りが重い
Department が workplace_id しか持たない設計です。正規化としては自然ですが、素朴な要求でも毎回 WorkPlace 経由のJOINが必要になります。
- 一覧・検索・権限制御など「よくある処理」ほどクエリが複雑になりがち
- 例外(事業所なし部署等)が出た瞬間に設計・実装の歪みが表面化する
✅ ソリューション:ER図は少し冗長、読み取りが単純
Department にも company_id を持たせます。各リソースが「自分がどの会社のデータか」を自己主張できるようにします。
- 会社配下の部署一覧が直感的に取得できる
- 取得・更新ロジックが単純になり、画面追加・仕様変更に強くなる
- 「部署がどの会社のものか」を認可やログでも扱いやすい
トレードオフ(この設計にもコストはある)
もちろん、company_id を冗長に持つ設計が「万能」なわけではありません。明確な弱点もあります。
それは、所属関係が変わったときの更新コストが増えることです。
たとえば M&A などで「会社と事業所(WorkPlace)の紐付け」が変わるケースを考えてみます。
-
正規化案(美しいER図):
WorkPlace.company_idを1箇所更新すれば終わり -
冗長案(今回推奨):
WorkPlace.company_idに加えて、配下にある膨大なDepartment.company_idもすべて更新が必要
読み取りが楽になる代償として、更新時の手間や、不整合リスクを自ら背負い込んでいるわけです。
いうほど問題か?と思う方へ
CompanyとWorkPlaceの関係を変えたのに、
Departmentに変更が必要になるのはやや違和感がありますよね。
それでも「冗長案」が正しいと言い切れる「根本要因」
では、なぜそのコストを払ってまで company_id を持たせるのか。
実は今回のシステムは、「1つのデータベースを、多数の契約企業で共有して利用する」という性質(マルチテナントSaaS)を持っていました。
このようなシステムにおいて、最も避けるべき致命傷は「A社の画面に、B社のデータが混ざって表示されること」です。
更新の手間というデメリットを飲み込んででも、この設計を貫くべき根拠。それは、もしもの時に他社へのデータ流出を物理的に防ぐ安全保障にあります。
「更新の手間」というデメリットを飲み込んででも、「他社のデータが混ざる」という致命傷を物理的に防ぐセーフティネットを買っているのです。
「他社のデータが混ざる」なんてほんまにあるんかいな!
ほんまにあるそうです。以下がその例で、読むとスッキリすると思います。読んでみてください。
① 論理削除による切断(幽霊データ問題)
親(事業所)を論理削除した瞬間に、JOINの鎖がプツリと切れます。すると、子(部署)がどの会社のものか辿れなくなる「幽霊データ」が生まれます。
Department.company_id があれば、親がどういう状態であれ、データの「所有権」だけはDB上に刻印され続けます。
② 人為的ミス(データ移行・SQL)へのガードレール
SQLで紐付けを書き換えるミスは、どんなに気をつけても起きます。その際、もし company_id が各テーブルになければ、一瞬で「A社の部署がB社の画面に出る」という事故が起きます。
全テーブルに company_id があれば、WHERE company_id = ? という一文が、他社への流出を食い止める最後の防波堤になります。
③ 認可バグに対する「最後の砦」
アプリのコードに「他人のデータを見れてしまうバグ」が混入する可能性はゼロにできません。
しかし、DB層で常に company_id をキーに動く設計にしていれば、仮にアプリが暴走しても、DBが「お前の会社のデータじゃない」と拒絶してくれます。
まとめ
結局のところ、DB設計は「何を優先し、何を捨てるか」の決断です。
- 捨てたもの: ER図の美しさ、所属変更時の更新の楽さ
- 得たもの: 開発効率(クエリの単純さ)と、マルチテナントとしての圧倒的な堅牢性
「JOINが減ってラッキー」という現場のメリットも大きいですが、それ以上に 「事故ったときでも、会社の境界だけは死守する」。
この 「実利」と「安全」の両取り ができるのが、今回たどり着いた非正規化の正体です。
🚀 ぼくが開発したサイト『GPAナビ』
この記事の知見や試行錯誤を詰め込んで開発・運営しているサービスです。
🎓 GPAナビ | 自分の立ち位置がわかる成績管理ツール
個人開発から始まり、現在では大阪大学の学生1,500人(阪大生の10人に1人以上!)、全体で4,000人のユーザーに利用される規模まで成長しました。
- 「自分のGPA、平均より高いのかな?」
- 「この学部の成績分布ってどうなってるの?」
そんな大学生のリアルな悩みを、統計データで解決します。
もし周りに大学生の方がいたら、ぜひシェアしていただけると嬉しいです!