1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「1=りんご、2=みかん」だけでは理解できなかった、正規化とマスタデータの話

1
Posted at

はじめに(導入)

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=みかん」という最初のイメージだけでは、後半で出てきた多対多・中間テーブル・複合キーの話まではカバーできません。マスタデータは「コード→名前の変換表」だけでなく、「どの組み合わせが正しいか」を管理する仕組みまで含めて、初めて正しく理解できるんだな、というのが今回の一番の学びでした。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?