結論:テーブル設計は「登場する“モノ”を洗い出す → モノごとに表を分ける → 各行を1つに特定できるキーを決める → 表と表を外部キーでつなぐ → 制約で守る」の順に進めます。正規化とは、この「モノごとに表を分ける」作業に第1〜第3正規形という段階の名前を付けたものです。 難しい理論のように見えますが、原則はたった一つ、「1つの事実は1か所にだけ書く」 だけです。
この記事は「テーブルをどう設計するか」という作り手側の手順に絞っています。SQLの学習順序そのものは データベース初心者は何から始めるか、JOIN の書き方は SQL JOINがわからない人向け で扱っています。
なぜ「1枚の表」ではダメなのか
設計の話は、悪い例から入ると一気にわかります。ショップの注文を、何も考えず1枚の表で持ったとします。
| 注文ID | 会員名 | 都市 | 商品(複数) | 単価 |
|---|---|---|---|---|
| 5 | 田中 | 福岡 | ノートPC, USBケーブル | 128000, 800 |
| 6 | 田中 | 福岡 | USBケーブル | 800 |
一見これで用は足りそうですが、運用を始めた瞬間に3種類の事故が起きます。教科書では更新時異常・挿入時異常・削除時異常と呼ばれるものです。
- 更新時異常:田中さんが福岡から大阪へ引っ越した。都市は注文の数だけコピーされているので、全部直さないといけない。1件でも直し忘れると「福岡の田中」と「大阪の田中」が同時に存在する、どちらが正しいか誰にもわからないデータになります。
- 挿入時異常:まだ1件も売れていない新商品を登録したい。でもこの表は「注文」の表なので、注文がないと商品を記録できません。
- 削除時異常:注文6を取り消したら、USBケーブルという商品の単価800円という情報まで一緒に消えてしまいます。
つまり、「事実が複数の場所に書かれている」ことと「別々の事実が1つの表に同居している」ことが原因です。表を分ければ全部防げます。この出発点は なぜ表を「分けて」設計するのか でそのままSQLを打ちながら確認できます。ブラウザ上で本物のSQLエンジンが動くので、インストールは要りません。
正規化の手順 ― 第1〜第3正規形
上の表を、3段階で整えていきます。これが正規化です。
第1正規形(1NF)― 1マスに1つの値
「商品(複数)」の欄に ノートPC, USBケーブル と2つ詰め込むのをやめ、1商品=1行にばらします。
なぜかというと、1マスに複数値が入っていると「USBケーブルを買った人を数える」ことすらできないからです。SQLは列の値を比較する言語なので、WHERE 商品 = 'USBケーブル' はカンマ区切りの文字列にヒットしません。
ここで生まれるのが order_items(注文明細)という表です。「注文1件に明細が複数ぶら下がる」という関係で、これを1対多と呼びます。関係の種類は リレーションの種類 ― 1対多という関係 で図解しています。
第2正規形(2NF)― 主キーの一部で決まる列を追い出す
注文明細のキーは「注文ID+商品ID」の組み合わせです。ではその行にある「単価」は何で決まるでしょうか。商品IDだけで決まります。注文が変わっても商品の定価は変わりません。
このように「キーの一部だけで決まってしまう列」は、その一部をキーとする別の表へ移します。単価は products(商品マスタ)へ引っ越し、商品マスタが独立します。
これを守らないと、商品の値段を改定したときに、その商品を含む全明細を書き換える羽目になります。
第3正規形(3NF)― キー以外の列にぶら下がる列を追い出す
注文表に「会員名」「都市」を持っているのが最後の問題です。注文IDが決まれば会員IDが決まり、会員IDが決まれば都市が決まる。都市は注文ではなく会員の属性です。キーではない列(会員ID)に別の列(都市)がぶら下がっている状態を推移的関数従属と呼び、これを切り離すと users(会員マスタ)が独立します。
結果、1枚の表は users / products / orders / order_items の4つに分かれ、最初に挙げた3つの異常がすべて消えます。この流れは 正規化 ― 重複を断つ設計の型 で、実際のデータを結合し直しながら追えます。
3つの正規形を一言でまとめる
| 段階 | やること | 覚え方 |
|---|---|---|
| 第1正規形 | 1マスに1値・繰り返しを行に分解 | 表の形を整える |
| 第2正規形 | キーの一部で決まる列を分離 | 商品マスタが生まれる |
| 第3正規形 | キー以外に依存する列を分離 | 会員マスタが生まれる |
「とりあえず第3正規形まで」とよく言われるのは、この3段階で更新・挿入・削除の異常がほぼ起きなくなるからです。第4・第5正規形もありますが、まず3NFを手で作れることが先です。
キーと制約が設計を守る
正規化して表を分けたら、次は「その分け方をデータベース自身に守らせる」段階です。ここを飛ばすと、せっかく分けた表が数か月でぐちゃぐちゃになります。
主キー(PRIMARY KEY) は「この列の値を見れば行が1つに特定できる」という宣言です。会員なら会員ID、注文なら注文ID。名前や電話番号を主キーにしないのは、改名・改番があり得るからで、多くの場合は連番のIDを自動採番させます。作り方は 主キーと自動採番 ― IDを自動で振る を実際に動かすのが早いです。
外部キー(FOREIGN KEY) は「この列の値は、あちらの表に必ず存在する」という宣言です。存在しない会員IDの注文を入れようとするとデータベースが拒否してくれるので、迷子データが生まれません。外部キーと参照整合性 ― 迷子のデータを防ぐ では、わざと違反させてエラーを出しながら確かめられます。
そのほか、NOT NULL・UNIQUE などの基本的な制約は 制約 ― 正しいデータだけを守る、値そのものの範囲を縛る CHECK や既定値の DEFAULT は CHECK と DEFAULT ― 値そのものを縛る にまとまっています。テーブルを作る文法自体があやふやなら データ型と CREATE TABLE ― テーブルを作る から。いずれもブラウザ上でそのまま CREATE TABLE を実行できる回です。
制約は「面倒なもの」ではなく「バグを本番前に止めてくれる仕組み」です。制約に弾かれたときのエラーは決まった文言なので、FOREIGN KEY constraint failed や UNIQUE constraint failed を読めば、何がまずかったかすぐ分かります。
多対多はどう設計するか
「1人の会員が複数の商品を買う」「1つの商品が複数の会員に買われる」のような多対多の関係は、そのままでは表現できません。答えは決まっていて、間に中間テーブルを1枚置くことです。先ほどの order_items がまさにそれで、注文と商品を結ぶ役をしています。
タグ付け(記事とタグ)、受講登録(学生と授業)など、業務システムの多くの「〜が〜を複数持つ」は中間テーブルで解けます。作り方は 多対多(N:M)と中間テーブル を参照してください。
どこまで正規化するか ― 実務の落としどころ
正規化は万能ではありません。表を細かく分けるほど、データを取り出すときの JOIN が増えて、クエリは複雑になり読み取りは遅くなります。
現場での基本方針はシンプルです。
- まず第3正規形で設計する。 迷ったら分ける。正しさを先に確保する。
- 遅い場所が実際に測れてから、非正規化(あえて重複を持つ)を検討する。想像で先回りしない。
- 非正規化するなら、重複を同期する責任が誰にあるかを決めてからにする。集計値を別表に持つなら、それを更新する処理を必ず用意する。
「遅い」の対処は、設計を崩す前にまず索引で解決できることが多いです。検索に使う列に索引を張る話は インデックス ― 検索を速くする索引、複合索引の考え方は 複合・ユニークインデックス にあります。索引を増やせば読み取りは速くなる一方で書き込みは重くなる、というトレードオフも実行しながら確認できます。
なお、そもそも関係データベース以外を選ぶ判断もあります。その比較は SQLとNoSQLの違い にまとめました。
設計の進め方(実際の手順)
紙の上でやることは、ほぼこの順番です。
- 要件を文章で書く。 「会員が商品を注文する」。この文の名詞がテーブル候補、動詞が関係になります。
- モノごとに表を分ける。 会員・商品・注文・注文明細。
- 各表の主キーを決める。 1行を一意に特定できるか声に出して確認する。
- 関係を線で結ぶ。 1対多か多対多か。多対多なら中間テーブルを足す。
- 列を配る。 「この列は本当にこの表の事実か?」と1列ずつ問う(=第3正規形の確認)。
- 制約を書く。 NOT NULL・UNIQUE・外部キー・CHECK。
CREATE TABLEに落とす。 ここで初めてSQLを書く。
この流れを図にしたものがER図です。書き方と読み方は データモデリングと ER 図 ― 設計の地図を描く、要件から実装まで通しでやる回は 設計から実装まで ― 総合演習 にあります。
つまずきやすいところ
- 「とりあえず全部の列を1つの表に」から抜け出せない。 列を足すのは簡単ですが、後から表を割るのは既存データの移行が伴って一番つらい作業です。設計時に分けておくのが結局いちばん安い。
- 主キーに意味のある値を使ってしまう。 メールアドレスや社員番号は変更されることがあります。変わり得る値をキーにすると、参照している全表に影響します。
- 外部キーを張らずに運用する。 「アプリ側でチェックしているから大丈夫」は、バッチ処理や手作業のUPDATEで簡単に破られます。行を消したときに何が起きるかは DELETE ― 行を削除する(と参照整合性) で確認できます。
- NULLを安易に許す。 「値が無い」は「0」でも「空文字」でもない第三の状態で、比較の挙動が変わります。挙動は NULL ― 「値が無い」を正しく扱う を実行して体感するのが確実です。
次に読む・次に動かす
- テーブル設計コース(sql-ddl) ― 設計から
CREATE TABLE・制約・索引まで、ブラウザで実行しながら進む無料コース(登録不要)。 - SQL(データ操作)コース ― 作った表に対して取り出す・集計する・結合する側。
- SQLのエラー逆引き ― 制約違反や構文エラーの意味と直し方。
- 用語で確認したい場合:正規化・主キー・外部キー・インデックス・リレーショナルデータベース。