はじめに(導入)
DB設計の勉強をしていると、必ず「正規化」という言葉に出会います。
私が最初にイメージした正規化は、
「コードと名前を1対1で対応させる表を作ること」
でした。
1 = りんご
2 = みかん
3 = ぶどう
こういう「コードマスタ」を用意すれば、DBにいちいち「りんご」という文字列を保存しなくて済みますし、名前を変更したくなったらマスタの1行を直すだけで済みます。これが正規化のメリットだと思っていましたし、これ自体は間違っていません。
ただ、業務であるコード調査(車両登録APIの「ミッション(AT/MT)」の妥当性チェックロジック)を担当したときに、自分の中の「マスタ」のイメージが崩れる場面に遭遇しました。
「あれ、コードと名前が1対1じゃないぞ…?」
「しかも同じコードなのに、この車種では使えて、この車種では使えない…?」
正直、最初は「コードだけで判定できないってどういうことだ」と混乱しました。
整理していくと、これは正規化の話としてもう一段深いところにある、
「コードの意味を決めるマスタ」と「そのコードがどの相手と組み合わせて良いかを決めるマスタ」は別物
という話でした。
この記事では、
- コードと名前は本当に1対1なのか
- 「意味としては正しいコードなのに使えない」とはどういうことか
を、身近な例(スマホのカラーバリエーション)に言い換えながら整理していきます。「1=りんご、2=みかん」のイメージだけでマスタデータを理解していた人(過去の自分)に向けて書いています。
正規化とは何か
正規化を一言でいうと、
「同じ情報をあちこちに重複して持たせず、1つの場所にまとめる」設計手法
です。
具体例で考えてみます。
正規化する前(非正規化)
商品テーブルに、商品名を直接文字列で保存するケースです。
| 商品ID | 商品名 |
|---|---|
| 1 | りんご |
| 2 | りんご |
| 3 | みかん |
| 4 | みかん |
| 5 | みかん |
ここで、「りんご」の表記を「アップル」に変えたくなったとします。すると、商品ID=1と2の両方を書き換えないといけません。100件、1000件あったら大変ですし、1件だけ直し忘れたら「りんご」と「アップル」が混在してデータが矛盾したままになります。
正規化した後
商品名を別テーブル(マスタ)に切り出し、商品テーブル側はコードだけを持つようにします。
商品マスタ:
| コード | 商品名 |
|---|---|
| 1 | りんご |
| 2 | みかん |
商品テーブル:
| 商品ID | コード |
|---|---|
| 1 | 1 |
| 2 | 1 |
| 3 | 2 |
| 4 | 2 |
| 5 | 2 |
これで「りんご→アップル」への変更は、商品マスタの1行を直すだけで全部に反映されます。これが「1=りんご、2=みかん」という、導入で使ったあの例の正体です。
つまり、
正規化とは、「情報を1つの場所に集約して、重複と矛盾の余地をなくす」ための整理術
と言えます。厳密には「第一正規形」「第二正規形」…と段階を踏んで定義されていますが、この記事では「重複を排除する」という一番大事な感覚だけ持っておけば十分です。
マスタとは何か
さっきの正規化の例で切り出した、あの「商品マスタ」を思い出してください。
商品マスタ:
| コード | 商品名 |
|---|---|
| 1 | りんご |
| 2 | みかん |
これがマスタ(マスタデータ)です。 「コードの意味を定義する側」「他のテーブルから参照される側」のテーブルのことをこう呼びます。
一方、これを参照していた「商品テーブル」(商品ID→コードを持つ方)は、マスタとは呼びません。あちらはマスタを参照する側のテーブルです。
商品テーブル:
| 商品ID | コード |
|---|---|
| 1 | 1 |
| 2 | 1 |
| 3 | 2 |
つまり正規化によって、
- コード→名前の対応表を切り出した方 → マスタ
- コードだけを持つようになった元のテーブル → マスタを参照する側
という役割分担が生まれます。「マスタ」という言葉は、正規化した結果として片方の役割に付く名前なんです。
身近な例
普段の生活でも、意識せずにマスタのお世話になっています。
- 都道府県マスタ(1=北海道、2=青森県…)→ 住所入力のプルダウン
- 郵便番号マスタ → 郵便番号から住所を自動入力する機能
- 銀行コード×支店コードマスタ → 支店コード「001」だけでは支店が決まらず、銀行コードと組み合わせて初めて意味が確定する
中でも銀行コード×支店コードは、「マスタ同士を組み合わせないと正しい意味・正しい判定にならない」という、次の章に繋がる感覚を先取りできる例です。
業務システムでの例
私が携わっている業務システムにも、コードで管理された項目(マスタ)がいくつもあります。
- ◯◯マスタ:◯◯コードと名称の対応
- △△マスタ:△△コードと名称の対応
- ミッション区分マスタ:ミッションコードと「AT」「MT」などの名前の対応
このミッション区分マスタこそが、次の章で「あれ、コードと名前が1対1じゃないぞ?」という違和感の発端になった主役です。
スマホのカラーコードで考える「意味は正しいのに使えない」
ここからは、冒頭で触れた「あれ、コードと名前が1対1じゃないぞ?」の正体を、スマホのカラーバリエーションに例えて考えてみます。
こんな「カラーコードマスタ」があるとします。
| コード | 名称 |
|---|---|
| 1 | ブラック |
| 2 | ホワイト |
| 3 | レッド |
| 4 | ゴールド |
段階1:コードから名前は分かるのに、名前からコードは分からない
さっきのカラーコードマスタ、実は3番の周りをもう少し詳しく見ると、こうなっていました。
| コード | 名称 |
|---|---|
| 3 | レッド |
| 5 | レッド |
コード3は通常色の「レッド」、コード5は限定色の「レッド」として、マスタの中でははっきり別の行として管理されています。
ここでのポイントは、変換の向きです。
コード → 名前 3 → レッド/5 → レッド(できる)
名前 → コード レッド → ?(3か5か決められない)
「コードが分かれば名前が分かる」のは一方通行で成り立ちますが、逆に「名前だけを渡されて、元のコードを教えてください」と言われても、3番か5番か決められません。名前は表示のための言い換えに過ぎず、コードの代わりに使える一意な手がかりではないんです。
実際に自分が担当した調査でも、これがそのまま問題になりました。「表示名からコードを一意に決定する方法はありますか?」という問い合わせに対する答えは、「ない。名前が同じでもコードが違う組み合わせが実在するから」でした。
つまり、
「名前」はコードの言い換えでしかなく、名前だけでは元のコードを一意に逆算できない
ことがある、というのが1つ目の崩れです。
段階2:コードの意味は正しいのに、使えないことがある
次はもう一段深い話です。
「コード3=レッド」という意味自体は、どのモデルでも変わりません。ここは崩れていません。
でも、
- 「iPhone SEにコード3(レッド)ってあります?」→ ある
- 「iPhone 15 Proにコード3(レッド)ってあります?」→ ない(Proにはレッドの展開がない)
というふうに、そのコードを「使えるかどうか」は、カラーコードマスタだけを見ても分かりません。「どのモデルにどの色が実在するか」を管理する、別の組み合わせ表を見て初めて分かることです。
カラーコードマスタだけ見て「3番は定義されているから使えるはず」と判断すると、本来存在しない「レッドのiPhone 15 Pro」を登録できてしまう、という事故が起きます。
業務システムで実際に起きていたこと(ミッション区分マスタの実例)
これと全く同じ構造のことが、私が担当した調査でも起きていました。ここでは実際の値ではなく、説明用に単純化した架空の値で説明します。
ミッション区分マスタには、次のようなコードがあるとします。
| コード | 名称 |
|---|---|
| 101 | 5MT |
| 102 | 5MT |
| 103 | 6MT |
コード101と102は、どちらも表示上は「5MT」です。実際にはシフトレバーの位置(フロアシフト/コラムシフトなど)が違う別仕様として管理されているのですが、名前だけ見ると同じ「5MT」に見えます。これが段階1の実例です。
さらに、この「ミッション区分マスタ」はコードの意味を定義するだけで、「どの車種にどのミッションが実在するか」までは教えてくれません。それは別の「車種×ミッションの組み合わせテーブル」が管理しています。
- 車種Aに実在する組み合わせ:
{101, 103} - 車種Bに実在する組み合わせ:
{102}
ここで、コード103(6MT)を車種Bに登録しようとすると、コードの意味としては全く正しい(6MTという意味は変わらない)のに、「車種Bには6MT仕様が実在しない」という理由でエラーになります。これが段階2の実例で、スマホの「レッドという色コード自体は正しいのに、iPhone 15 Proにはレッドの展開がない」という状況と全く同じ構造です。
つまり、
- ミッション区分マスタ=カラーコードマスタ(コードの意味を決める)
- 車種×ミッションの組み合わせテーブル=モデルごとの色展開表(どの組み合わせが実在するかを決める)
という、役割の違う2種類のマスタが存在していて、片方だけを見ていると正しい判定ができないんです。
銀行の支店コードとの違い
さっき「身近な例」で出した銀行コード×支店コードの話も「組み合わせないと確定しない」という点では似ていますが、実は少し種類が違います。
- 銀行コード×支店コード:組み合わせないと「どの支店を指すか(意味)」が決まらない
- ミッション区分マスタ×車種:組み合わせなくても「意味」は決まっている。組み合わせないと「その組み合わせが実在するか(使えるか)」が決まらない
同じ「組み合わせ」でも、「意味を確定させるための組み合わせ」と「存在・妥当性を確認するための組み合わせ」の2種類があるわけです。マスタデータを扱うときは、この2つを混同しないことが大事だと感じました。
「組み合わせ表」の正式名称:多対多の関連(中間テーブル)
ここまで「車種とミッションの組み合わせを管理する別テーブル」というふうに説明してきましたが、これには正規化の教科書に出てくる正式な名前があります。
1対多の関係なら、実は列を1つ増やすだけで済む
例えば「都道府県」と「市区町村」の関係を考えてみます。
- 1つの都道府県には、複数の市区町村がある
- でも1つの市区町村は、1つの都道府県にしか属さない
これは**1対多(one-to-many)**の関係です。この場合、市区町村テーブルに「都道府県コード」という列を1つ追加するだけで表現できます。新しいテーブルを作る必要はありません。
でも、車種とミッションは両方向で「複数」になる
一方、車種とミッションの関係はこうなっていました。
- 1つの車種には、複数のミッションが存在する(車種Aは101も103も持つ)
- 1つのミッションも、複数の車種で使われる(103は車種Aだけでなく、別の車種でも使われる)
このように両方向で「複数」になる関係を、**多対多(many-to-many)**と呼びます。
図にすると分かりやすいので、車種Cも登場させて線を引いてみます。
線があちこちで交差しているのが分かると思います。ミッション103は車種AともCとも繋がっていますし、車種Cはミッション102ともミッション103とも繋がっています。「1つの車種」を主語にしても「1つのミッション」を主語にしても、相手は複数いる。これが多対多です。
多対多になると、1対多のときのように片方のテーブルに列を1つ追加するだけでは表現できません。もし無理にやろうとすると、
車種テーブル:
| 車種コード | ミッションコード |
|---|---|
| A | 101, 103 |
のように、1つのセルに複数の値をカンマ区切りで詰め込むしかなくなります。でもこれは第一正規形(1つのセルには1つの値しか入れない、というルール)に違反してしまいます。検索も更新もしづらく、正規化が目指す「重複と矛盾の排除」からも遠ざかってしまいます。
だから「組み合わせ専用のテーブル」を作る
正規化のルールを守りながら多対多の関係を表現するには、車種とミッションの組み合わせを1行1レコードとして持つ、専用の第3のテーブルを用意します。これを**中間テーブル(関連テーブル)**と呼びます。
車種×ミッション 中間テーブル:
| 車種コード | ミッションコード |
|---|---|
| A | 101 |
| A | 103 |
| B | 102 |
これなら1セル1値のルールを守ったまま、「どの車種にどのミッションが実在するか」を正しく表現できます。実は前章で紹介した「車種×ミッションの組み合わせテーブル」は、まさにこの中間テーブルだったわけです。
実際に値を入れてみると、こうなります(本記事で使ってきた架空の値です)。
ミッション区分マスタ(コードの意味を決める)
| ミッションコード | 名称 |
|---|---|
| 101 | 5MT |
| 102 | 5MT |
| 103 | 6MT |
中間テーブル(どの車種にどのミッションが実在するかを決める)
| 車種コード | ミッションコード |
|---|---|
| A | 101 |
| A | 103 |
| B | 102 |
| C | 102 |
| C | 103 |
この2つのテーブルを使うと、「車種Bにミッション103は登録できる?」という問い合わせに、次のような手順で答えが出せます。
これを実際の数値で追ってみます。
-
車種B + ミッション103:①中間テーブルを車種コード=Bで絞り込むと
{102}だけが残る → ②103は含まれていない → NG -
車種C + ミッション103:①中間テーブルを車種コード=Cで絞り込むと
{102, 103}が残る → ②103が含まれている → OK
ここで大事なのは、ミッション区分マスタを見る限り、103は「6MT」という意味として何も間違っていないということです。それでも車種Bには登録できません。「コードの意味を決めるマスタ」と「実在する組み合わせを決めるマスタ(中間テーブル)」を分けて、①→②の順で確認して初めて、正しい判定ができるわけです。
中間テーブルの主キーは「複合キー」になっている
さっきの中間テーブルを、もう一度見てみます。
| 車種コード | ミッションコード |
|---|---|
| A | 101 |
| A | 103 |
| B | 102 |
| C | 102 |
| C | 103 |
このテーブルの各行を一意に特定するための「主キー(プライマリキー)」は何になるでしょうか。
- 車種コードだけでは一意にならない(Aだけで2行ある)
- ミッションコードだけでも一意にならない(102も103も、それぞれ2行ある)
- でも車種コード+ミッションコードの組み合わせなら、必ず1行だけに絞れる
このように、2つ以上の列を組み合わせて初めて一意になるキーのことを、**複合キー(複合主キー)**と呼びます。
実はこれ、記事の前半で紹介した「銀行コード×支店コードマスタ」も同じ考え方でした(支店コードだけでは一意にならず、銀行コードと組み合わせて初めて1つの支店に決まる)。
ただ、今回の中間テーブルの複合キーには、もう少し深い意味があります。この複合キーの行が存在すること自体が、「その組み合わせが実在する」という事実を表しているんです。
-
(A, 103)という行が存在する → 車種Aに103という組み合わせが実在する -
(B, 103)という行は存在しない → 車種Bに103という組み合わせは実在しない
銀行コード×支店コードの複合キーは「どの支店か」という意味を一意に決めるための複合キーでした。一方、車種×ミッションの複合キーは、意味はもう決まった上で「その組み合わせが実在するかどうか」を1行の有無で表現するための複合キーです。同じ「複合キー」でも、役割が少し違います。
ここまで整理してきた「意味は正しいのに使えない」という違和感は、突き詰めると「その複合キーの行が存在するかどうか」の話だった、ということになります。
「マスタとマスタを組み合わせないと正しい判定にならない」という感覚の裏側には、多対多の関係を正規化のルールに従って解消すると、自動的に中間テーブルが必要になるという、ちゃんとした理論的な理由があったんです。
まとめ
長くなったので、要点を整理します。
- 正規化とは、同じ情報をあちこちに重複させず、1つの場所に集約する設計手法
- マスタとは、正規化によって切り出された「コードの意味を決める側」のテーブルのこと
- ただし「コードの意味が正しい」ことと「その組み合わせが実在する・使える」ことは別の話
- 名前とコードが1対1ではないことがある(段階1)
- コードの意味自体は変わらないのに、組み合わせ次第で使える/使えないが決まることがある(段階2)
- こうした「組み合わせの妥当性」を管理しているのが、多対多の関係を解消する中間テーブル
- 中間テーブルの各行は、複合キー(2つ以上のコードが揃って初めて一意になるキー)で管理されている
- つまり中間テーブルの1行が存在すること自体が、「その組み合わせが実在する」という事実を表している
「1=りんご、2=みかん」という最初のイメージだけでは、後半で出てきた多対多・中間テーブル・複合キーの話まではカバーできません。マスタデータは「コード→名前の変換表」だけでなく、「どの組み合わせが正しいか」を管理する仕組みまで含めて、初めて正しく理解できるんだな、というのが今回の一番の学びでした。