← Back to HomeSeptember 8, 2025
SQL & RDBMS構築の基礎(非正規形から第四正規形)

SQL & RDBMS構築の基礎(非正規形から第四正規形)

SQLRDBMS正規化トランザクション

SQLのRDBMSの構築方法を非正規形から正規化し設計思想や効率的な方法をまとめています。

はじめに

このページではリレーショナルDBと正規化の有用性とSQLでの正規化されたDBの作成の仕方を記述しています


正規化とは

正規化とは、一言で言えば**「データベースの整理整頓術」**のことです。データベース内のテーブルを、ある一定のルールに従って分割し、管理しやすく綺麗な状態にすることを指します。

多くの人が最初にデータベースを学ぶ際、Excelのシートのように1枚の大きな表にすべての情報を詰め込んでしまいがちです。しかし、この方法ではデータが複雑になるにつれて、様々な問題が発生します。

正規化は、特定のSQLコマンドを指す言葉ではなく、そうした問題を未然に防ぐための**「テーブルの最適な設計思想(ルール)」**であり、効率的で信頼性の高いデータベースを構築するための基礎となる非常に重要な概念です。

リレーショナルDBとは

正規化を理解するためには、まずその舞台となる「リレーショナルデータベース(RDB)」の基本的な仕組みを知る必要があります。

リレーショナルデータベースとは、データを複数の「表(テーブル)」に分割して管理し、それぞれのテーブルを関連性(リレーション)で結びつけるデータベースのことです。

例えば、ECサイトのデータベースを考えてみましょう。

  • 顧客テーブル: 顧客のID、名前、住所、電話番号などを管理
  • 商品テーブル: 商品のID、商品名、価格などを管理
  • 注文テーブル: 注文ID、注文日、どの顧客が(顧客ID)、どの商品を(商品ID)、いくつ注文したか、を管理

このように、「顧客」「商品」「注文」といった情報の種類ごとにテーブルを分けます。そして、注文テーブルに「顧客ID」と「商品ID」を持たせることで、「この注文は、どの顧客が、どの商品を買ったものか」という関連性を表現します。

この**「データを意味のある単位で分割し、IDなどで関連付ける」**という特徴が、リレーショナルデータベースの核心であり、正規化はこの分割をより論理的かつ効率的に行うための手法なのです。

正規化をおこなう目的

では、なぜわざわざテーブルを分割して「整理整頓(正規化)」する必要があるのでしょうか。その目的は、大きく分けて2つあります。

1. データの冗長性(無駄な重複)をなくすため

もし正規化を行わず、1枚の巨大な注文管理表を作ると、ある顧客が買い物をするたびに、その顧客の名前や住所が繰り返し記録されてしまいます。これが**データの冗長性(無駄な重複)**です。

正規化を行い、「顧客テーブル」と「注文テーブル」に分割すれば、顧客情報は顧客テーブルに1つだけ記録すればよくなります。これにより、データベース全体の容量を節約でき、スリムで効率的なデータ管理が可能になります。

2. データの不整合(矛盾)を防ぐため【最重要】

こちらが正規化を行う最も重要な目的です。データの不整合とは、同じ意味を持つデータなのに、場所によって値が異なってしまうという矛盾した状態を指します。

具体例:顧客の住所変更

  • 正規化されていない場合: 顧客Aさんが3回買い物をしていると、データベースにはAさんの住所が3ヶ所に記録されています。もしAさんが引っ越した場合、その3ヶ所すべての住所を正確に更新しなければなりません。1つでも更新を忘れると、「ある注文記録では旧住所、別の注文記録では新住所」という**データの矛盾(不整合)**が発生してしまいます。

  • 正規化されている場合: Aさんの住所は「顧客テーブル」にただ1つだけ記録されています。そのため、Aさんが引っ越した際は、顧客テーブルの1ヶ所の住所を更新するだけで済みます。注文テーブルからは常に最新の顧客情報が参照されるため、更新漏れによるデータの矛盾は決して発生しません。

このように、正規化はデータの更新・追加・削除といった操作を行う際に発生しがちな「更新時異常」や「削除時異常」といった不整合を防ぎ、データの正確性と信頼性(整合性)を担保するために不可欠な設計手法なのです。

以上の考えを元に正規化されたデータベースを作成する方法をステップアップで記述します。


ステップ0: すべてが混在した「非正規形」のテーブル

まずは、正規化が全く行われていない、以下のような「注文管理テーブル」を例に考えてみましょう。

注文管理テーブル(非正規形)

注文ID 注文日 顧客ID 顧客名 顧客住所 商品情報
1001 2025-08-28 C01 田中 太郎 東京都... (A01,ペン,100円,2個), (B02,ノート,150円,1冊)
1002 2025-08-28 C02 鈴木 花子 神奈川県... (C03,消しゴム,50円,5個)
1003 2025-08-29 C01 田中 太郎 東京都... (B02,ノート,150円,3冊)

このテーブルには、以下のような問題があります。

  • 商品情報列に、複数の商品の情報が繰り返し入っている。
  • 顧客情報(顧客名、顧客住所)が注文のたびに重複して記録されている。
  • 商品名や単価が変わった場合、過去のすべての関連レコードを修正する必要があり、修正漏れのリスクがある。

ステップ1: 第一正規形(1NF)への道 - 繰り返しの排除

第一正規形は、1つのセル(フィールド)に1つの値しか含まない状態を目指します。 つまり、商品情報列のような繰り返し項目を解消します。

手法: 「注文」と「注文した商品の詳細」を別のテーブルに分割します。

実践SQLコマンド (CREATE TABLE, INSERT INTO):

-- 元のテーブルから注文情報を抽出して新しいテーブルを作成
CREATE TABLE Orders (
    order_id INT PRIMARY KEY,
    order_date DATE,
    customer_id VARCHAR(255)
);

INSERT INTO Orders (order_id, order_date, customer_id)
VALUES
    (1001, '2025-08-28', 'C01'),
    (1002, '2025-08-28', 'C02'),
    (1003, '2025-08-29', 'C01');

-- 注文明細情報を格納するテーブルを作成
CREATE TABLE Order_Details (
    order_detail_id INT PRIMARY KEY,
    order_id INT,
    product_id VARCHAR(255),
    product_name VARCHAR(255),
    unit_price INT,
    quantity INT
);

INSERT INTO Order_Details (order_detail_id, order_id, product_id, product_name, unit_price, quantity)
VALUES
    (1, 1001, 'A01', 'ペン', 100, 2),
    (2, 1001, 'B02', 'ノート', 150, 1),
    (3, 1002, 'C03', '消しゴム', 50, 5),
    (4, 1003, 'B02', 'ノート', 150, 3);

これで、繰り返しが解消され、第一正規形が達成できました。

ステップ2: 第二正規形(2NF)への道 - 部分関数従属の排除

第二正規形は、複合主キーの一部だけで一意に決まる項目を分離することを目指します。 これを「部分関数従属を解消する」と言います。

Order_Detailsテーブルの主キーを(order_id, product_id)(注文IDと商品IDの組み合わせ)だと考えた場合、product_name(商品名)やunit_price(単価)はproduct_idだけで決まります。order_idには依存しません。これが部分関数従属です。

手法: 主キーの一部にのみ依存する列を、さらに別のテーブルに分離します。

実践SQLコマンド (ALTER TABLE, DROP COLUMN):

-- 商品情報を管理するProductsテーブルを作成
CREATE TABLE Products (
    product_id VARCHAR(255) PRIMARY KEY,
    product_name VARCHAR(255),
    unit_price INT
);

INSERT INTO Products (product_id, product_name, unit_price)
VALUES
    ('A01', 'ペン', 100),
    ('B02', 'ノート', 150),
    ('C03', '消しゴム', 50);

-- Order_Detailsテーブルから冗長な列を削除
ALTER TABLE Order_Details DROP COLUMN product_name;
ALTER TABLE Order_Details DROP COLUMN unit_price;

正規化後のテーブル構成:

  • Orders (注文ID, 注文日, 顧客ID)
  • Order_Details (注文明細ID, 注文ID, 商品ID, 数量)
  • Products (商品ID, 商品名, 単価)

ステップ3: 第三正規形(3NF)への道 - 推移的関数従属の排除

第三正規形は、主キー以外の列に依存する列を分離することを目指します。 これを「推移的関数従属を解消する」と言います。

最初の非正規形テーブルにあった顧客情報を例に考えてみましょう。Customersテーブルを作成します。

CREATE TABLE Customers (
    customer_id VARCHAR(255) PRIMARY KEY,
    customer_name VARCHAR(255),
    address VARCHAR(255)
);
INSERT INTO Customers (customer_id, customer_name, address)
VALUES
    ('C01', '田中 太郎', '東京都...'),
    ('C02', '鈴木 花子', '神奈川県...');

もしこのCustomersテーブルに部署IDと部署名があった場合、部署名は部署IDによって決まります(主キーのcustomer_idから直接決まるわけではない)。これが推移的関数従属です。

手法: 主キー以外の列に依存している列を、さらに別のテーブルに分離します。

最終的なテーブル構成 (第三正規形):

  • Customers (顧客ID, 顧客名, 住所)
  • Orders (注文ID, 注文日, 顧客ID)
  • Order_Details (注文明細ID, 注文ID, 商品ID, 数量)
  • Products (商品ID, 商品名, 単価)

これで、データの重複と不整合のリスクが大幅に削減された、メンテナンスしやすいデータベース構造になりました。

ステップ4: 第四正規形(4NF)以降(プロフェッショナル向け)

第四正規形(4NF)や第五正規形(5NF)は、より複雑なデータの依存関係(多値従属性や結合従属性)を扱うためのものですが、実務上は第三正規形まで正規化されていれば十分な場合がほとんどです。

  • 第四正規形 (4NF): 1つのレコードに、互いに独立した複数の情報(例えば「社員」に対して「扱える言語」と「所属プロジェクト」など)が複数存在する場合に、それを分離します。

実践的SQLテクニック

正規化によって分割されたテーブルから、必要な情報を効率的に取り出すための実践的なSQLコマンドを解説します。

1. データベースの呼び出しと連結 (SELECT, JOIN)

分割されたテーブルの情報をつなぎ合わせて、意味のあるデータを取得します。

  • INNER JOIN: 両方のテーブルに存在するデータのみを結合します。
  • LEFT (OUTER) JOIN: 左側のテーブルの全データと、それに一致する右側のテーブルのデータを結合します。一致しない場合はNULLが入ります。
  • RIGHT (OUTER) JOIN: 右側のテーブルを基準に結合します。
  • FULL OUTER JOIN: 両方のテーブルの全データを結合し、一致しない箇所はNULLで補います。

実践例:注文ID 1001の注文詳細(顧客名、商品名、単価、数量)を取得する

SELECT
    c.customer_name,
    p.product_name,
    p.unit_price,
    od.quantity
FROM
    Orders o
INNER JOIN
    Customers c ON o.customer_id = c.customer_id
INNER JOIN
    Order_Details od ON o.order_id = od.order_id
INNER JOIN
    Products p ON od.product_id = p.product_id
WHERE
    o.order_id = 1001;

2. ソートと集計 (ORDER BY, GROUP BY)

  • ORDER BY: 結果を指定した列で並べ替えます (ASCで昇順、DESCで降順)。
  • GROUP BY: 指定した列の値でグループ化し、集計関数(SUM, AVG, COUNTなど)を適用します。
  • HAVING: GROUP BYで集計した結果に対して、さらに条件を指定します。

実践例:顧客ごとの合計注文金額を計算し、金額が高い順に表示する

SELECT
    c.customer_name,
    SUM(p.unit_price * od.quantity) AS total_amount
FROM
    Customers c
JOIN
    Orders o ON c.customer_id = o.customer_id
JOIN
    Order_Details od ON o.order_id = od.order_id
JOIN
    Products p ON od.product_id = p.product_id
GROUP BY
    c.customer_name
ORDER BY
    total_amount DESC;

3. 条件分岐 (CASE式)

SQLクエリ内でif文のような条件分岐を実現します。データの表示形式を動的に変えたり、複雑な集計を行ったりする際に非常に強力です。

実践例:単価に応じて商品のランクを表示する

SELECT
    product_name,
    unit_price,
    CASE
        WHEN unit_price >= 500 THEN '高ランク'
        WHEN unit_price >= 100 THEN '中ランク'
        ELSE '低ランク'
    END AS price_rank
FROM
    Products;

4. 安全なデータ編集とトランザクション

INSERT, UPDATE, DELETEといったデータ操作は、誤ると重大な問題を引き起こします。そこで重要になるのがトランザクションです。 トランザクションとは、「すべて成功するか、すべて失敗するかのどちらか」を保証する一連の処理の単位です。

  • BEGIN TRANSACTION (または START TRANSACTION): トランザクションを開始します。
  • COMMIT: トランザクション内のすべての変更をデータベースに確定します。
  • ROLLBACK: トランザクション内のすべての変更を取り消し、開始前の状態に戻します。

実践例:銀行振込のように、A口座からB口座への移動を安全に行う

BEGIN TRANSACTION;

-- A口座の残高を減らす
UPDATE Accounts SET balance = balance - 10000 WHERE account_id = 'A';

-- B口座の残高を増やす
UPDATE Accounts SET balance = balance + 10000 WHERE account_id = 'B';

-- もし途中でエラーが発生したら、両方の処理が取り消される。
-- すべて成功した場合のみ、以下のCOMMITで変更が確定する。
COMMIT;

まとめ

  • 正規化は設計思想: データベースを整理整頓するためのルールであり、CREATE TABLEやALTER TABLEを駆使して手動で設計・実装します。
  • JOINが鍵: 正規化で分割されたテーブルは、JOIN句を使って効率的に連結し、必要な情報を引き出します。
  • 高度なテクニック: GROUP BY, CASE式, サブクエリなどを組み合わせることで、複雑なデータ分析や操作が可能になります。
  • 安全性: トランザクション処理は、データの整合性を保つ上で不可欠なプロの必須スキルです。

Other Learning Topics

SQLコマンド一覧

SQLコマンド一覧

SQLのコマンドを表にしてコマンド、目的、使用状況の例、実行例をまとめています。

C言語 データ型の一覧

C言語 データ型の一覧

C言語のデータ型の型指定子、バイト長の大きさ、値の範囲を符号付きと符号なし、浮遊小数点に分けてまとめています。

C言語 出力フォーマット指定子一覧

C言語 出力フォーマット指定子一覧

C言語の出力フォーマット指定子について指定子の記述法、対応するデータ型、表示のされ方を表にしてまとめています。