重複データを 1NF に変換 (フラットなリスト化)
重複データを 1NF に変換 (フラットなリスト化)
このレッスンで分かること
- 1NF 変換のポイントは 3 つあります
- リレーショナルデータベース(RDB)の世界では、テーブルが満たすべきルールを「正規形」と呼びます
- 例えば次のような顧客テーブルがあるとします
重複データを 1NF に変換 とは
1 セルに複数値が詰め込まれた CSV 風データを、1 行 = 1 値にフラット化する第一正規形(1NF)の変換を実装します。
リレーショナルデータベース(RDB)の世界では、テーブルが満たすべきルールを「正規形」と呼びます。その出発点が 第一正規形(1NF) です。1NF を一言で言うと、「1 つのセル(カラムの値)に複数の値を詰め込まない」 という決まりです。
例えば次のような顧客テーブルがあるとします。
| user_id | name | hobbies |
|---|---|---|
| 1 | Alice | reading, music, hiking |
| 2 | Bob | gaming |
| 3 | Carol | cooking, travel |
この hobbies カラムには "reading, music, hiking" のように 複数の値がカンマ区切り で入っています。一見便利に見えますが、ここから「music が趣味のユーザーは誰?」を検索するには、文字列の中身を LIKE '%music%' で探すしかなく、index が効きません。hobbies を 1 行 1 値に分解した次の形が 1NF です。
| user_id | name | hobby |
|---|---|---|
| 1 | Alice | reading |
| 1 | Alice | music |
| 1 | Alice | hiking |
| 2 | Bob | gaming |
| 3 | Carol | cooking |
| 3 | Carol | travel |
1NF の本質は「カラムを
atomic(これ以上分解できない単位)にする」こと。
なぜ 1NF にしないと困るのか
1NF を満たさないと、データベースの基本機能がほとんど使えません。WHERE での絞り込み、JOIN での結合、GROUP BY での集計、COUNT SUM などの集計関数、すべて 「1 セル = 1 値」前提 で設計されています。
SQL クエリ
-- 1NF 違反: hobbies = 'reading, music, hiking' のまま
SELECT * FROM users WHERE hobbies = 'music'; -- 一件もヒットしない
-- 1NF: 1 行 1 趣味
SELECT * FROM user_hobbies WHERE hobby = 'music'; -- index で一発「カラムに JSON 配列を入れればいいのでは?」と思うかもしれません。RDB の
JSON型でも検索はできますが、B-tree索引が部分一致では効かず、性能・スキーマ管理ともに不利になります。
配列を「縦に展開する」処理を flatten と呼ぶ
1NF への変換は、プログラミング的には 配列(list)を縦方向に平坦化(flatten)する 処理に相当します。1 件の入力レコードから複数件の出力レコードを生成するので、map だけでは表現できず、flat_map や 二重ループ が必要になります。
コードでの考え方
入力は [name, comma_separated_values] のペアのリスト。出力は [name, single_value] のペアのリスト。Python なら次のように書けます。
Python
def normalize1NF(rows):
result = []
for name, csv in rows:
for value in csv.split(","):
value = value.strip()
if value:
result.append([name, value])
return resultJavaScript の場合は flatMap が便利です。
JavaScript
function normalize1NF(rows) {
return rows.flatMap(([name, csv]) =>
csv.split(",").map(v => v.trim()).filter(v => v).map(v => [name, v])
);
}1NF 変換のポイントは 3 つあります。
splitで分解、trimで前後の空白除去、filterで空文字を捨てる。
よくある間違い
空文字を残してしまうのが代表的な失敗です。"a, , b" を単純に split(",") すると ["a", " ", "b"] になり、空白だけの要素が出力に混入します。trim してから if value で除外する必要があります。
また、split の区切り文字を ", " (カンマ + 空白)にすると "a,b" (空白なし)の入力で分解できません。区切り文字は , のみ にして、空白除去は trim に任せるのが安全です。
やってみよう
配列 [["Alice", "reading, music"], ["Bob", "gaming"]] を入力に、[["Alice", "reading"], ["Alice", "music"], ["Bob", "gaming"]] を返す関数 normalize1NF を実装してください。split trim filter の組み合わせで、1NF の世界を体感できます。
よくある質問
Q. なぜ正規化するのですか?
A. データの重複を排除し、更新時の不整合(更新異常・挿入異常・削除異常)を防ぐためです。第 3 正規形まで適用すると保守性が大きく上がります。一方で JOIN が増えるため、検索性能と保守性のバランスで非正規化することもあります。
Q. 1NF と 2NF の違いは?
A. 1NF は「各セルが単一値」、2NF は「主キーの一部だけに依存するカラムをテーブル分割」する形です。複合キーの片方だけに紐づく属性は別テーブルに切り出すと 2NF になります。1NF を満たさないと 2NF 以降の議論はできません。
Q. 全部正規化すれば最適ですか?
A. 理想ですが、JOIN が増えてパフォーマンスが落ちる場面では非正規化(カラム複製、計算結果のキャッシュ)も有効です。OLTP は 3NF、レポーティング用 DWH はスタースキーマ(非正規化)と、用途で使い分けるのが現代的なアプローチです。
次のレッスン
次は キーで join (map 利用) に進みましょう。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- 1NF 正規化 の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. 1NF 正規化 とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- カンマで区切られた文字列を 1 値ずつ分解する
- 各値の前後の空白を
trimして取り除く - 空文字(trim 後に長さ 0)は出力に含めない
入出力例
test-cases.txt
normalize1NF([["Alice","reading, music, hiking"],["Bob","gaming"]]) → [["Alice","reading"],["Alice","music"],["Alice","hiking"],["Bob","gaming"]]
normalize1NF([["Carol","cooking,travel"]]) → [["Carol","cooking"],["Carol","travel"]]
normalize1NF([["Dave"," movies , music "]]) → [["Dave","movies"],["Dave","music"]]
normalize1NF([["Eve","art, , drawing"]]) → [["Eve","art"],["Eve","drawing"]]
normalize1NF([["A","x,y"],["B","z,w"]]) → [["A","x"],["A","y"],["B","z"],["B","w"]]