Day26 前半のゴール
「“とりあえず動く設計”から“一生付き合える設計”に育てる感覚をつかむ」
Day25 までで、顧客・商品・注文のテーブルを分けて、
それなりに“ちゃんとした”売上管理ができるようになりました。
Day26 のテーマは 設計改善:テーブル分割・正規化。
ゴールはこうです。
「一応動いているテーブル設計」を眺めて、
どこにムダや重複があるかを見つける
それを「テーブル分割」「正規化」という形で改善するイメージを持つ
ここでは、難しい理論を増やすというより、
「今ある設計を、どう“育てていくか”」という視点を身につけることを重視します。
まずは“ちょっとダメな設計”をあえて用意する
「最初から完璧じゃなくていい、むしろそこからが設計の本番」
いきなり正しい正規化の話をしても、
「ふーん」で終わりがちです。
なので、あえて少しダメなテーブルを出します。
例えば、こんな顧客テーブルを想像してください。
CREATE TABLE customers_bad (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL,
phone TEXT,
zip_code TEXT,
address TEXT,
prefecture TEXT,
city TEXT,
street TEXT
);
SQL一見、普通に使えそうです。
でも、よく見ると「住所っぽい情報」がバラバラに入っています。
zip_code と address が両方ある
prefecture / city / street と address が重複している
住所の持ち方のルールが曖昧
こういう「なんとなく増やしたカラム」が、
あとからじわじわ効いてきます。
ここから、「どこをどう分けるとスッキリするか」を考えるのが、
設計改善・正規化の入り口です。
正規化を“現場の言葉”に言い換える
「同じことを何度も書かない」「意味の違うものを同じ場所に置かない」
正規化という言葉は堅いですが、
やっていることはシンプルです。
同じ情報を何度も書かないようにする
意味の違う情報を、同じカラムや同じテーブルに押し込まない
例えば、顧客テーブルに「都道府県名」を文字列で持っているとします。
東京都
東京
都内
のように、表記ゆれが起きます。
これを放置すると、「都道府県別集計」が地獄になります。
ここで出てくるのが「テーブル分割」です。
都道府県を別テーブルにして、
顧客テーブルからは「都道府県ID」だけを持つようにする。
これが、正規化を現場の言葉にしたときの一つの形です。
住所を例にしたテーブル分割
「“マスタ”と“実データ”を分ける」
さっきの customers_bad を、少しマシな形にしてみます。
まず、「都道府県」をマスタテーブルにします。
CREATE TABLE prefectures (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);
SQLここに、
1, '北海道'
2, '青森県'
…
13, '東京都'
…
のように、都道府県一覧を入れておきます。
次に、顧客テーブル側はこう変えます。
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
phone TEXT,
prefecture_id INTEGER,
city TEXT,
street TEXT
);
SQLポイントは二つです。
都道府県名そのものは持たず、prefecture_id だけを持つ
住所を「都道府県」「市区町村」「番地・建物」など、意味ごとに分ける
これにより、
都道府県名の表記ゆれがなくなる
都道府県名を変更したいとき、prefectures だけ直せばよい
顧客テーブルのカラムが少しスッキリする
という効果が出ます。
これが、「マスタテーブルに切り出す」という典型的な正規化の一歩です。
「1つのカラムに複数の意味を詰め込まない」という感覚
「カンマ区切り・スラッシュ区切りは“危険信号”」
もう一つ、よくある設計の悪い例を出します。
CREATE TABLE customers_bad2 (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL,
tags TEXT -- 例: 'VIP,メルマガ,キャンペーンA'
);
SQL一見便利そうですが、tags にカンマ区切りで複数の情報を詰め込んでいます。
「VIP の顧客だけ取りたい」と思ったとき、WHERE tags LIKE '%VIP%' のような、
雑な検索をせざるを得ません。
これは、正規化の観点から見ると完全にアウトです。
本来は、「顧客」と「タグ」は別テーブルにして、
中間テーブルで多対多の関係を表現するのが筋です。
CREATE TABLE tags (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE customer_tags (
customer_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
PRIMARY KEY (customer_id, tag_id)
);
SQLこうしておけば、
「VIP タグを持つ顧客」
「メルマガタグを持つ顧客」
などを、JOIN で素直に取れるようになります。
ここでの重要な感覚は、
1つのカラムに「複数の値」や「複数の意味」を詰め込んだら負け
ということです。
カンマ区切り・スラッシュ区切り・JSON 文字列などは、
「本当は別テーブルにすべき情報を無理やり押し込んでいる」サインになりがちです。
正規化とパフォーマンスの関係をざっくり押さえる
「まずは“きれいに分ける”、そのあと“必要ならまとめる”」
正規化の話をすると、よく出てくる心配がこれです。
「テーブルを分けすぎると、JOIN が増えて遅くなるのでは?」
これは半分正しくて、半分誤解です。
設計の基本方針としては、
まずは正規化して“意味的にきれいな形”にする
その上で、本当に必要な箇所だけ、ビューやキャッシュテーブルで“まとめておく”
という順番が健全です。
最初から「JOIN が面倒だから 1 テーブルに詰め込む」とやると、
短期的には楽でも、長期的には必ず破綻します。
一方で、
「分析用に、顧客×都道府県×売上をまとめたビューを作る」
「バッチで日次集計テーブルを作っておく」
といった“読み取り専用のまとめ方”は、
正規化と矛盾しません。
Day26 では、
正規化=テーブルを意味ごとに分ける
パフォーマンス対策=必要に応じて“読み取り用のまとめ”を別途用意する
という切り分けを、頭の中に置いておいてください。
セキュリティの視点から見た設計改善
「“どこに何があるか”をはっきりさせることが、守りの第一歩」
正規化やテーブル分割は、
セキュリティ的にも大きな意味を持ちます。
例えば、
顧客テーブルに「パスワードハッシュ」「クレジットカード情報」「住所」「権限フラグ」など、
何でもかんでも詰め込んでいるとします。
この状態だと、
「このテーブルにアクセスできる人は、全部見えてしまう」
という危険な状況になります。
一方で、
認証情報は user_credentials
権限は user_roles
個人情報は customer_profiles
のように分けておけば、
「このアプリは顧客プロフィールだけ参照」
「このバッチは売上だけ参照」
といった形で、アクセス範囲を絞りやすくなります。
つまり、
正規化=意味ごとにテーブルを分けることは、
「どこに何があるか」を明確にし、
「どこまで見せるか」を制御しやすくする、という意味で
セキュリティの土台にもなります。
Day26 前半のまとめ
設計改善・正規化の出発点は、「今あるテーブルのどこに重複や意味の混在があるか」を見つけること。
住所や都道府県のような“共通で使う情報”は、マスタテーブルに切り出し、元のテーブルからは ID だけを持たせることで、表記ゆれや修正漏れを防げる。
1つのカラムに複数の値(カンマ区切りなど)や複数の意味を詰め込むのは、正規化の観点から危険信号であり、本来は別テーブルや中間テーブルで表現すべき関係であることが多い。
正規化は「まず意味的にきれいに分ける」ためのもので、パフォーマンスが必要な箇所はビューや集計テーブルなど“読み取り専用のまとめ”で補う、という役割分担を意識するとよい。
テーブルを意味ごとに分けておくことは、「どこに何の情報があるか」を明確にし、アクセス範囲を絞りやすくするという意味で、セキュリティ設計の土台にもなる。
後半では、
実際に「悪い設計 → 改善後の設計」を具体的な例で並べて比較しながら、
第1〜第3正規形の考え方を“用語抜きでも説明できるレベル”まで落とし込んでいきます。
