顧客が引っ越して住所を更新した。ところが古い注文の画面では、まだ前の住所が表示されている——同じ「顧客の住所」というデータを、複数の注文レコードにコピーして持っていたのが原因です。1箇所を直しても、コピーされた他の場所は直らずに残ります。
これが更新異常で、正規化はこの種の矛盾を構造的に防ぐ技術です。この記事は理論の暗記ではなく、実際にコードを実行して矛盾が起きる様子とその解決を見せます。1NF・2NF・3NFという用語は最後に、必要な分だけ触れます。
正規化は用語の暗記ではなく「同じ事実を1箇所にだけ持つ」設計。この記事のコードは実際に矛盾を再現し、テーブル分割だけで解消している。
DB設計と正規化の理論は、E.F. Coddが1970年代に定式化した第三正規形(3NF)の定義に集約されます。“every non-prime attribute of R is non-transitively dependent on each candidate key”。各非キー属性が、候補キーに間接的にではなく直接依存していること——これが定義です。難解な表現ですが、実際に起きる問題を見てから読むと理解が変わります。
以下は編集部が実際に実行したコードと、その出力です。顧客の住所を複数の注文行にコピーして持つ「未正規化」のテーブルで、住所を更新してみます。
// ❌ 未正規化: 顧客の住所を複数の注文行に重複して持つ
let ordersFlat = [
{ orderId: 1, customerId: "C1", customerAddress: "東京都渋谷区1-1", item: "ノートPC" },
{ orderId: 2, customerId: "C1", customerAddress: "東京都渋谷区1-1", item: "モニター" },
{ orderId: 3, customerId: "C2", customerAddress: "大阪府大阪市2-2", item: "キーボード" },
];
function updateAddressFlat(customerId, newAddress) {
const target = ordersFlat.find((o) => o.customerId === customerId);
target.customerAddress = newAddress; // うっかり1行だけ更新
}
=== 未正規化テーブルで C1 の住所を更新 ===
[
{ "orderId": 1, "customerId": "C1", "customerAddress": "東京都新宿区9-9", "item": "ノートPC" },
{ "orderId": 2, "customerId": "C1", "customerAddress": "東京都渋谷区1-1", "item": "モニター" },
{ "orderId": 3, "customerId": "C2", "customerAddress": "大阪府大阪市2-2", "item": "キーボード" }
]
→ orderId:1 は新住所、orderId:2 は旧住所のまま。同じ顧客なのにデータが矛盾(更新異常)
orderId 1と2は同じ顧客C1のはずですが、更新後は住所が食い違っています。これが更新異常で、原因はコードのバグではなくデータ構造そのもの——同じ事実(C1の住所)を2行に重複して持ったことです。
A relation R is in 3NF if and only if it is in second normal form (2NF) and every non-prime attribute of R is non-transitively dependent on each candidate key.出典Wikipedia「Third normal form」(E.F. Coddの定義の引用元) 一次情報を確認2026-08-14 この内容の有効期限2027-02-14
解決策は複雑な理論ではなくテーブル分割。顧客テーブルを切り出し外部キーで参照すれば、更新は1箇所で済み矛盾が起きようがなくなる。
DB設計と正規化の実務的な解決は、重複しているデータを別テーブルに切り出すことです。同じ実行環境で、正規化した後のテーブルに同じ更新をしてみます。
=== 正規化後のテーブルで C1 の住所を更新 ===
[
{ "orderId": 1, "customerId": "C1", "item": "ノートPC", "address": "東京都新宿区9-9" },
{ "orderId": 2, "customerId": "C1", "item": "モニター", "address": "東京都新宿区9-9" },
{ "orderId": 3, "customerId": "C2", "item": "キーボード", "address": "大阪府大阪市2-2" }
]
→ 1箇所の更新で全注文の住所が一致。矛盾が構造的に起きない
顧客テーブルを分離し、注文テーブルはcustomerIdで参照するだけにしました。住所を更新する処理は顧客テーブルの1行を触るだけで済み、矛盾を起こす経路そのものが構造からなくなります。このテーブル分割の発想はクリーンアーキテクチャで扱う「詳細を後から差し替え可能にする」考え方とも通じます。
| 正規形 | 点検すること | 違反の例 |
|---|---|---|
| 1NF | 1つのセルに複数の値を詰めていないか | tagsカラムに"AI,自動化,SaaS"とカンマ区切りで格納 |
| 2NF | 複合キーの一部にしか依存しない列がないか | 受注明細テーブルに商品名(商品IDだけに依存)を直接持つ |
| 3NF | 非キー列が他の非キー列に依存していないか(間接依存) | 注文テーブルに顧客住所(顧客IDに依存、注文IDには非依存)を持つ |
まず正規化して始め、実測してから崩す。最初から非正規化するのは、問題が起きる前に複雑さを買うことになる。
DB設計と正規化を学ぶと、全てを正規化したくなりますが、行き過ぎには代償があります。正規化するほどテーブルが増え、1件の表示に必要なJOINも増えます。アクセス頻度が高い画面では、この分割自体が性能のボトルネックになることがあります。
在庫数のように頻繁に参照される集計値を、都度JOINして計算する代わりに非正規化した列として持ち、更新のたびに計算し直す設計もあります。これは正規化を知らないミスではなく、意図的なトレードオフです。ただし順番が重要で、最初は正規化して設計し、実際のアクセスパターンを計測してから、必要な箇所だけ崩すのが安全です。
個人開発の初期段階では、パフォーマンス問題が顕在化する前に非正規化を持ち込むと、更新異常のリスクだけを先取りして背負うことになります。編集部の見立てでは、最初は素直に正規化し、ボトルネックが実測されてから崩すのが、個人開発として現実的な優先順位です。
同じ課題を持つ会社にとって、動いている設定は「作る時間」を買えるということです。ServiceDockは自作のワークフローやテンプレートを出品できるマーケットプレイスです。手数料や出品の流れは出品者向けページにまとまっています。
出品の仕組みを見る