重複データを 1NF に変換 (フラットなリスト化)

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

重複データを 1NF に変換 (フラットなリスト化)

このレッスンで分かること

  • 1NF 変換のポイントは 3 つあります
  • リレーショナルデータベース(RDB)の世界では、テーブルが満たすべきルールを「正規形」と呼びます
  • 例えば次のような顧客テーブルがあるとします

重複データを 1NF に変換 とは

1 セルに複数値が詰め込まれた CSV 風データを、1 行 = 1 値にフラット化する第一正規形(1NF)の変換を実装します。

リレーショナルデータベース(RDB)の世界では、テーブルが満たすべきルールを「正規形」と呼びます。その出発点が 第一正規形(1NF) です。1NF を一言で言うと、「1 つのセル(カラムの値)に複数の値を詰め込まない」 という決まりです。

例えば次のような顧客テーブルがあるとします。

user_idnamehobbies
1Alicereading, music, hiking
2Bobgaming
3Carolcooking, travel

この hobbies カラムには "reading, music, hiking" のように 複数の値がカンマ区切り で入っています。一見便利に見えますが、ここから「music が趣味のユーザーは誰?」を検索するには、文字列の中身を LIKE '%music%' で探すしかなく、index が効きません。hobbies を 1 行 1 値に分解した次の形が 1NF です。

user_idnamehobby
1Alicereading
1Alicemusic
1Alicehiking
2Bobgaming
3Carolcooking
3Caroltravel

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二重ループ が必要になります。

diagram (will load when visible)

コードでの考え方

入力は [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 result

JavaScript の場合は 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 つができることを確認しましょう。

  1. 1NF 正規化 の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. 1NF 正規化 とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. カンマで区切られた文字列を 1 値ずつ分解する
  2. 各値の前後の空白を trim して取り除く
  3. 空文字(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"]]

ヒント

main.py
main.py
学習モード

メモ

重複データを 1NF に変換 (フラットなリスト化)

⌘S で保存