1. はじめに
アプリケーション開発でデータベースを使う機会は多いですよね。
でも、「テーブル設計ってどうやるの?」「何を基準に考えればいいの?」
と悩んだことはありませんか?
良いデータベース設計は、アプリケーションの保守性やパフォーマンスに
大きく影響します。逆に、設計が悪いと後から修正するのは非常に困難です。
このシリーズでは、データベース設計の基礎を6回に分けて解説します。
第1回は、設計の出発点となる「エンティティ」と「ER図」について学びましょう。
1.1 このシリーズで学べること
- データベース設計の基本概念(ER図、エンティティ) ← 今回
- SQLの基本をマスターしよう
- 正規化入門(第1〜第3正規形)
- インデックスの仕組みと使い方
- トランザクションとACID特性
- 実践的なテーブル設計パターン集
- よくあるアンチパターンと対策
2. なぜデータベース設計が重要なのか
後から「このカラムが足りない」「このテーブル構造では対応できない」
となると、大規模な修正が必要になります。
2.1 良い設計のメリット
- データの整合性が保たれる
- 必要なデータが効率よく取得できる
- 機能追加や変更に柔軟に対応できる
- バグが発生しにくい
2.2 悪い設計のデメリット
- データの重複や不整合が発生しやすい
- クエリが複雑になり、パフォーマンスが悪化
- 後からの修正コストが莫大になる
設計段階でしっかり考えることが、後々の開発を楽にします。
3. エンティティ(Entity)とは
エンティティとは、データベースで管理したい「モノ」や「コト」の単位です。
現実世界の概念をデータベースで表現する際の基本単位と考えてください。
3.1 具体例
ECサイトを例に考えてみましょう。
- ユーザー (顧客情報を管理したい)
- 商品 (販売する商品情報を管理したい)
- 注文 (誰が何をいつ注文したか管理したい)
- カテゴリ (商品の分類を管理したい)
これらがエンティティになります。
エンティティは、通常データベースのテーブルとして実装されます。
3.2 エンティティの見つけ方
要件を分析する際、名詞に注目するとエンティティが見えてきます。
例:「ユーザーが商品を注文する」
- ユーザー → エンティティ
- 商品 → エンティティ
- 注文 → エンティティ
ただし、すべての名詞がエンティティになるわけではありません。
「管理する必要があるか」「独立した存在として扱うべきか」を考えましょう。
4. 属性(Attribute)とは
エンティティが持つ情報の項目を属性と呼びます。
データベースではカラム(列) として実装されます。
4.1 ユーザーエンティティの例
ユーザーエンティティには、どんな情報が必要でしょうか?
| 属性名 | 説明 | データ型 |
|---|---|---|
| user_id | ユーザーを一意に識別するID | INT |
| username | ユーザー名 | VARCHAR(50) |
| メールアドレス | VARCHAR(255) | |
| password_hash | パスワードのハッシュ値 | VARCHAR(255) |
| created_at | 登録日時 | TIMESTAMP |
| updated_at | 更新日時 | TIMESTAMP |
データ型は、次の項目で説明します。
4.2 属性の決め方のポイント
✅ そのエンティティ固有の情報か
- ユーザーの名前 → ユーザーエンティティに含める
- 注文の合計金額 → 注文エンティティに含める
❌ 他のエンティティの情報を持たせない
- ユーザーエンティティに「最新の注文日」を持たせる → NG
(注文エンティティから取得すべき)
属性は、そのエンティティを説明するために最小限必要な情報だけを持たせます。
5. データ型とは
カラムに保存できるデータの種類を指定するものです。
適切なデータ型を選ぶことで、以下のメリットがあります:
✅ ストレージの効率化 - 無駄な容量を使わない
✅ データの整合性 - 不正な値の登録を防ぐ
✅ パフォーマンスの向上 - 検索や計算が高速になる
5.1 主なデータ型一覧
📊 数値型
| データ型 | 説明 | 格納範囲 | 使用例 |
|---|---|---|---|
| TINYINT | 非常に小さな整数 | -128 〜 127 | フラグ、ステータス(0,1,2など) |
| INT | 整数 | -2,147,483,648 〜 2,147,483,647 | ID、数量、年齢 |
| BIGINT | 大きな整数 | -9,223,372,036,854,775,808 〜 ... | SNSのいいね数、アクセス数 |
| DECIMAL(M,D) | 固定小数点数 | M:全体桁数、D:小数点以下桁数 | 金額、価格(誤差が許されない) |
| FLOAT | 浮動小数点数(単精度) | 約7桁の精度 | 座標、評価スコア |
| DOUBLE | 浮動小数点数(倍精度) | 約15桁の精度 | 科学計算 |
例:
CREATE TABLE products (
product_id INT, -- 商品ID
price DECIMAL(10, 2), -- 価格(例: 12345.67)
stock_quantity INT, -- 在庫数
rating FLOAT -- 評価(例: 4.5)
);
💡 金額がDECIMALを使う理由:
-- FLOATだと誤差が出る可能性がある
SELECT 0.1 + 0.2; -- 結果: 0.30000000000000004 (場合によって)
-- DECIMALなら正確
SELECT CAST(0.1 AS DECIMAL(10,2)) + CAST(0.2 AS DECIMAL(10,2));
-- 結果: 0.30
📝 文字列型
| データ型 | 説明 | 最大長 | 使用例 |
|---|---|---|---|
| CHAR(N) | 固定長文字列 | 0〜255文字 | 都道府県コード(2文字)、性別(M/F) |
| VARCHAR(N) | 可変長文字列 | 0〜65,535文字 | 名前、メールアドレス、URL |
| TEXT | 長いテキスト | 0〜65,535文字 | 商品説明、コメント |
| MEDIUMTEXT | より長いテキスト | 0〜16,777,215文字 | 記事本文 |
| LONGTEXT | 非常に長いテキスト | 0〜4,294,967,295文字 | 書籍の全文データ |
例:
CREATE TABLE users (
username VARCHAR(50), -- 可変長(実際の長さに応じて容量使用)
gender CHAR(1), -- 固定長(常に1文字分の容量)
bio TEXT, -- 自己紹介文
prefecture_code CHAR(2) -- '01'(北海道)〜'47'(沖縄)
);
💡 CHARとVARCHARの違い:
- CHAR(10)に'abc'を保存 → 'abc○○○○○○○' (10文字分の領域確保)
- VARCHAR(10)に'abc'を保存 → 'abc' (3文字分+α の領域のみ)
💡 使用例
- 固定長のデータ(都道府県コード、国コードなど)→ CHAR
- 可変長のデータ(名前、メールなど)→ VARCHAR
📅 日付・時刻型
| データ型 | 説明 | フォーマット | 使用例 |
|---|---|---|---|
| DATE | 日付のみ | YYYY-MM-DD | 誕生日、契約日 |
| TIME | 時刻のみ | HH:MM:SS | 営業開始時刻 |
| DATETIME | 日付+時刻 | YYYY-MM-DD HH:MM:SS | イベント開始日時 |
| TIMESTAMP | タイムスタンプ | YYYY-MM-DD HH:MM:SS | 作成日時、更新日時 |
| YEAR | 年のみ | YYYY | 卒業年 |
例:
CREATE TABLE events (
event_date DATE, -- 2026-02-02
event_time TIME, -- 14:30:00
event_datetime DATETIME, -- 2026-02-02 14:30:00
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
💡 DATETIMEとTIMESTAMPの違い:
-- DATETIME: 1000-01-01 〜 9999-12-31 (タイムゾーン非対応)
-- TIMESTAMP: 1970-01-01 〜 2038-01-19 (タイムゾーン対応、自動更新可能)
-- 作成日時・更新日時にはTIMESTAMPが便利
CREATE TABLE posts (
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
✅ 真偽値型
| データ型 | 説明 | 格納値 | 使用例 |
|---|---|---|---|
| BOOLEAN | 真偽値 | TRUE(1) / FALSE(0) | フラグ、有効/無効 |
| TINYINT(1) | BOOLEANの実体 | 0または1 | MySQLではBOOLEANはTINYINT(1)のエイリアス |
例:
CREATE TABLE users (
is_active BOOLEAN DEFAULT TRUE, -- アカウントが有効か
is_admin BOOLEAN DEFAULT FALSE, -- 管理者権限があるか
email_verified BOOLEAN DEFAULT FALSE -- メール認証済みか
);
-- データの登録
INSERT INTO users (username, is_active, is_admin)
VALUES ('taro', TRUE, FALSE);
-- クエリ
SELECT * FROM users WHERE is_active = TRUE;
🗂️ その他の型
| データ型 | 説明 | 使用例 |
|---|---|---|
| ENUM('値1','値2',...) | 列挙型(決まった値のみ) | ステータス、カテゴリ |
| JSON | JSON形式データ | 設定情報、メタデータ |
| BLOB | バイナリデータ | 画像、ファイル(非推奨) |
例:
CREATE TABLE orders (
order_id INT PRIMARY KEY,
status ENUM('pending', 'confirmed', 'shipped', 'delivered', 'cancelled'),
metadata JSON -- {"shipping_method": "express", "gift_wrap": true}
);
-- ENUMの使用例
INSERT INTO orders (order_id, status) VALUES (1, 'pending');
-- 不正な値は登録できない
INSERT INTO orders (order_id, status) VALUES (2, 'invalid'); -- エラー!
💡 ENUMの注意点:
- メリット: 不正な値を防げる、容量効率が良い
- デメリット: 値の追加・削除にALTER TABLEが必要
※ 値が頻繁に変わる場合は、別テーブル(マスタテーブル)を作る方が良い
CREATE TABLE order_statuses (
status_id INT PRIMARY KEY,
status_name VARCHAR(20)
);
5.2 データ型の選び方
✅ 数値データ
- NG: すべてBIGINTにする(無駄にストレージを使う)
user_id BIGINT -- 数億人ユーザーがいなければINTで十分
- OK: 必要な範囲に応じて選ぶ
user_id INT -- 21億まで対応(通常これで十分)
age TINYINT -- 0〜255で十分
price DECIMAL(10, 2) -- 金額は誤差が出ないように
✅ 文字列データ
- NG: すべてTEXT型にする(検索が遅くなる、インデックスが張りにくい)
username TEXT
email TEXT
- OK: 適切な長さを指定
username VARCHAR(50) -- 一般的なユーザー名の長さ
email VARCHAR(255) -- メールアドレスの最大長
description TEXT -- 長い説明文
✅ 日付・時刻データ
- NG: 文字列で保存
created_at VARCHAR(50) -- '2026-02-02 14:30:00'
- OK: 適切な日付型を使う
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
birthday DATE
5.3 属性の決め方のポイント
✅ そのエンティティ固有の情報か
- ユーザーの名前 → ユーザーエンティティに含める
- 注文の合計金額 → 注文エンティティに含める
❌ 他のエンティティの情報を持たせない
- ユーザーエンティティに「最新の注文日」を持たせる → NG
(注文エンティティから取得すべき)
✅ 適切なデータ型を選ぶ
- 金額 → DECIMAL (FLOATは誤差が出る)
- フラグ → BOOLEAN
- 長さが決まっている → CHAR
- 長さが可変 → VARCHAR
属性は、そのエンティティを説明するために最小限必要な情報だけを持たせます。
6. 主キー(Primary Key)とは
主キーは、テーブル内のレコードを一意 に識別するためのカラムです。
必ず設定する必要があります。
6.1 主キーの条件
- 一意性: 同じ値を持つレコードが存在しない
- NOT NULL: NULL値を許可しない
- 不変性: 一度設定したら変更しない
6.2 主キーの種類
1. 代理キー(サロゲートキー)
システムが自動生成する連番のID。最も一般的。
CREATE TABLE users (
user_id INT AUTO_INCREMENT PRIMARY KEY, -- 代理キー
username VARCHAR(50) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE
);
2. 自然キー(ナチュラルキー)
ビジネス上意味を持つ値をキーにする。
CREATE TABLE countries (
country_code CHAR(2) PRIMARY KEY, -- 'JP', 'US' など
country_name VARCHAR(100) NOT NULL
);
6.3 どちらを使うべき?
基本的には代理キーを推奨します。
理由:
- 自然キーは「変更されない」という保証がない(メールアドレスの変更など)
- 複合キー(複数カラムの組み合わせ)になると扱いづらい
- 外部キーとして参照する際にシンプル
自然キーは、国コードや商品コードなど「絶対に変わらない」ものに限定しましょう。
7. ER図(Entity Relationship Diagram)とは
エンティティ同士の関係を視覚的に表現した図です。
設計段階で全体像を把握するために非常に有用です。
7.1 ER図の記法
代表的な記法に「IE記法」と「カラスの足記法」があります。
ここでは、わかりやすいカラスの足記法を使います。
7.2 リレーションシップの種類
1. 1対1 (One to One)
一つのエンティティが、もう一つのエンティティの一つとだけ関連する。
例: ユーザー ←→ ユーザープロフィール
- 1人のユーザーは1つのプロフィールを持つ
- 1つのプロフィールは1人のユーザーに属する
[ユーザー] ||--|| [ユーザープロフィール]
2. 1対多 (One to Many)
一つのエンティティが、もう一つのエンティティの複数と関連する。
例: ユーザー ←→ 注文
- 1人のユーザーは複数の注文ができる
- 1つの注文は1人のユーザーに属する
[ユーザー] ||--o{ [注文]
3. 多対多 (Many to Many)
双方が複数と関連する。実装時には中間テーブル が必要。
例: 商品 ←→ カテゴリ
- 1つの商品は複数のカテゴリに属する
- 1つのカテゴリは複数の商品を持つ
[商品] }o--o{ [カテゴリ]
※多対多は次回以降で詳しく解説します。
7.3 カラスの足記法の読み方
記号の意味:
-
||: 必ず1つ -
o|: 0または1つ -
}o: 0以上 -
}{: 1以上
[親] ||--o{ [子]
これは「親は0個以上の子を持ち、子は必ず1つの親に属する」という意味です。
8. 実践例:ECサイトのユーザーと注文を設計してみよう
実際にER図とテーブル定義を作成してみましょう。
8.1 要件
- ユーザーは複数の注文ができる
- 注文には複数の商品が含まれる(1つの注文で複数商品を購入可能)
- 商品は在庫数を管理する
8.2 ER図
8.3 テーブル定義(SQL)
ユーザーテーブル
CREATE TABLE users (
user_id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_email (email) -- メールアドレスでの検索用
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
商品テーブル
CREATE TABLE products (
product_id INT AUTO_INCREMENT PRIMARY KEY,
product_name VARCHAR(100) NOT NULL,
price DECIMAL(10, 2) NOT NULL, -- 小数点2桁まで
stock_quantity INT NOT NULL DEFAULT 0,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CHECK (price >= 0), -- 価格は0以上
CHECK (stock_quantity >= 0) -- 在庫は0以上
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
注文テーブル
CREATE TABLE orders (
order_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
total_amount DECIMAL(10, 2) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, confirmed, shipped, delivered, cancelled
ordered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE RESTRICT,
INDEX idx_user_id (user_id),
INDEX idx_ordered_at (ordered_at),
CHECK (total_amount >= 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
注文明細テーブル
CREATE TABLE order_items (
order_item_id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL, -- 注文時点の単価を保存
FOREIGN KEY (order_id) REFERENCES orders(order_id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(product_id) ON DELETE RESTRICT,
INDEX idx_order_id (order_id),
INDEX idx_product_id (product_id),
CHECK (quantity > 0),
CHECK (unit_price >= 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
8.4 設計のポイント解説
1. 外部キー制約を設定
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE RESTRICT
-
ON DELETE RESTRICT: 親レコード削除時にエラーを出す(データの整合性を守る) -
ON DELETE CASCADE: 親が削除されたら子も削除(注文が削除されたら注文明細も削除)
2. なぜ注文明細テーブルにunit_priceを持たせるのか?
❌ 悪い設計: 商品テーブルの価格を参照する
-- これだと、商品の価格が変わったときに過去の注文金額が狂う
SELECT p.price FROM products p WHERE p.product_id = ?
✅ 良い設計: 注文時点の単価を保存
- 注文時の価格をorder_itemsに保存しておく
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES (1, 100, 2, 1500.00); -- 注文時点で1500円だった
商品の価格は変動する可能性があるため、注文時点の価格を注文明細に保存します。
3. CHECK制約でデータの妥当性を担保
CHECK (price >= 0) -- 負の価格は許可しない
CHECK (quantity > 0) -- 数量は1以上
アプリケーション側でもバリデーションしますが、DB側でも制約を設けることで二重の安全策になります。
9. 初心者がやりがちな設計ミス
❌ ミス1: 全部1つのテーブルに詰め込む
-- 悪い例: 注文情報をすべてordersテーブルに
CREATE TABLE orders (
order_id INT PRIMARY KEY,
user_name VARCHAR(50),
user_email VARCHAR(255),
product_name_1 VARCHAR(100), -- 商品1
product_price_1 DECIMAL(10,2),
product_name_2 VARCHAR(100), -- 商品2
product_price_2 DECIMAL(10,2),
-- 商品が増えたらどうする...?
);
問題点:
- 商品数が固定されている(柔軟性がない)
- データの重複(ユーザー情報が毎回保存される)
- 検索やメンテナンスが困難
解決策: エンティティを適切に分割する(前述の設計例を参照)
❌ ミス2: 主キーを設定しない
-- 悪い例
CREATE TABLE users (
username VARCHAR(50),
email VARCHAR(255)
);
問題点:
- レコードを一意に特定できない
- 重複データが入る可能性がある
- 更新・削除が困難
解決策: 必ずPRIMARY KEYを設定する
❌ ミス3: 外部キーを設定しない
-- 悪い例
CREATE TABLE orders (
order_id INT PRIMARY KEY,
user_id INT -- 外部キー制約なし
);
問題点:
- 存在しないuser_idを登録できてしまう
- データの整合性が保証されない
解決策: FOREIGN KEY制約を設定する
-- 良い例
FOREIGN KEY (user_id) REFERENCES users(user_id)
9. まとめ
今回はデータベース設計の基礎となる概念を学びました。
9.1 今回学んだこと
- エンティティ: 管理したいモノ・コトの単位(テーブルになる)
- 属性: エンティティが持つ情報(カラムになる)
- 主キー: レコードを一意に識別するためのカラム
- ER図: エンティティ間の関係を視覚化した図
- リレーションシップ: 1対1、1対多、多対多の関係
- データ型
9.2 設計のポイント
✅ エンティティは適切に分割する
✅ 必ず主キーを設定する
✅ 外部キー制約でデータの整合性を保つ
✅ ER図で全体像を把握する
9.3 次回予告
次回はSQLの基本をマスターしようについて学びます!
「テーブルは設計できたけど、どうやって作るの?🤔」
「データの追加・取得・更新・削除ってどうやるの?」
「SQLって難しそう...😰」
こんな疑問に答えるのが、次回のテーマSQL基礎です!
10. おわりに
最後まで読んでいただきありがとうございました!🎉
よろしければ他の記事もご覧頂けるとすごくうれしいです。
👍 いいね / 💬 コメントいただけると励みになります!