DB 設計の進め方
テーブルをいきなり CREATE するのではなく、業務を理解し、概念・論理・物理の3段階で設計を進めるのが基本です。 このページでは、受注・顧客・商品ドメインを題材に、設計の全工程を実践的に解説します。
このページは概念理解を優先して、一部のフィールド名を日本語で記載しています。 「命名規約」セクションで英語名への置き換え方針を示します。
設計フロー全体像
DB 設計は、大きく3段階に分かれます。各段階で扱う対象と成果物が異なります。
| 段階 | 目的 | 主な作業 | 成果物 |
|---|---|---|---|
| 概念設計 | 業務の世界を整理する | エンティティ抽出、関連の定義、多重度の決定 | 概念 ER 図 |
| 論理設計 | RDB で表現できる形にする | 属性・主キー・外部キーの定義、正規化、データ型の選定 | 論理 ER 図、テーブル定義書 |
| 物理設計 | DBMS に最適化する | インデックス設計、パーティション、ストレージ配置、DDL 作成 | DDL スクリプト、インデックス定義書 |
概念設計を飛ばしていきなり CREATE TABLE を書くと、後から「テーブルの分け方がおかしい」という手戻りが発生します。 まず業務を理解し、概念レベルで合意を取ることが最も重要です。
各段階で問うべき質問
- 概念設計: 「業務で管理すべき"もの"は何か? それらはどう関係しているか?」
- 論理設計: 「主キーは何か? 正規化されているか? データ型は適切か?」
- 物理設計: 「どの列にインデックスが要るか? 想定データ量で性能は足りるか?」
エンティティ抽出の実践
概念設計の最初のステップは、業務要件からエンティティ(管理対象)を見つけ出すことです。 基本的な手法は「業務記述の名詞を拾う」ことです。
業務要件の例文
「顧客が受注を行う。 1回の受注には複数の商品を含めることができる。 各商品には単価とカテゴリがある。 受注ごとに受注日と合計金額を管理する。 顧客には顧客名、電話番号、住所を登録する。」
名詞の抽出と分類
| 抽出した名詞 | 分類 | 理由 |
|---|---|---|
| 顧客 | エンティティ | 独立して管理される対象 |
| 受注 | エンティティ | 独立して管理される対象(イベント系) |
| 商品 | エンティティ | 独立して管理される対象(マスタ系) |
| 顧客名 | 属性 | 顧客を説明する情報 → 顧客エンティティの属性 |
| 電話番号 | 属性 | 顧客を説明する情報 → 顧客エンティティの属性 |
| 住所 | 属性 | 顧客を説明する情報 → 顧客エンティティの属性 |
| 単価 | 属性 | 商品を説明する情報 → 商品エンティティの属性 |
| カテゴリ | エンティティ or 属性 | 種類が多い場合はエンティティ、少なければ属性 |
| 受注日 | 属性 | 受注を説明する情報 → 受注エンティティの属性 |
| 合計金額 | 導出値 | 明細から計算可能 → 保持するか要検討 |
「合計金額」のように他のデータから計算できる値(導出値)は、テーブルに持たせると更新不整合のリスクが生じます。 保持する場合は、更新タイミングと整合性の維持方法を明確にしてください。
エンティティの種類
| 種類 | 特徴 | 例 |
|---|---|---|
| リソース系(マスタ) | 比較的変更が少ない基本情報 | 顧客、商品、カテゴリ、部署 |
| イベント系(トランザクション) | 業務活動の記録。日時を伴うことが多い | 受注、出荷、請求、入金 |
関連と多重度(カーディナリティ)
エンティティ同士がどのように結びつくかを「関連」、その数量関係を「多重度(カーディナリティ)」と呼びます。 多重度には大きく3つのパターンがあります。
1:1(1対1)
一方のエンティティ1件に対して、もう一方も必ず1件だけ対応するケースです。
| 例 | 説明 |
|---|---|
| 顧客 : 顧客詳細 | 顧客の基本情報と拡張情報を分離する場合。列数が多すぎるテーブルの分割や、アクセス頻度の異なるデータの分離に使う |
| 社員 : ログインアカウント | 社員1名に対してアカウント1つ |
1:1 は実務では比較的少ないパターンです。「本当に分ける必要があるか」を常に問いましょう。
1:N(1対多)
最も頻出するパターンです。「親」側の1件に対して、「子」側が複数件対応します。
| 親(1側) | 子(N側) | 説明 |
|---|---|---|
| 顧客 | 受注 | 1人の顧客が複数回受注する |
| 受注 | 受注明細 | 1回の受注に複数の商品明細がある |
| カテゴリ | 商品 | 1つのカテゴリに複数の商品が属する |
実装では、子テーブルに親の主キーを外部キー (FK) として持たせます。
-- 1:N の実装例
CREATE TABLE customers (
customer_id INT PRIMARY KEY,
customer_name VARCHAR(100) NOT NULL
);
CREATE TABLE orders (
order_id INT PRIMARY KEY,
order_date DATE NOT NULL,
customer_id INT NOT NULL,
FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);
M:N(多対多)
両方のエンティティが、互いに複数件と対応するケースです。 RDB では M:N を直接表現できないため、中間テーブル(交差テーブル)を挟んで 1:N + N:1 に分解します。
| 例 | 説明 |
|---|---|
| 商品 : 受注 | 1つの商品が複数の受注に含まれ、1つの受注にも複数の商品が含まれる。中間テーブル = 受注明細 |
| 学生 : 講座 | 1人の学生が複数講座を受講し、1つの講座にも複数学生がいる。中間テーブル = 受講登録 |
M:N を中間テーブルで解決する ER 図
-- M:N を中間テーブルで解決する DDL
CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name VARCHAR(100) NOT NULL,
unit_price NUMERIC(10,2) NOT NULL
);
CREATE TABLE orders (
order_id INT PRIMARY KEY,
order_date DATE NOT NULL,
customer_id INT NOT NULL,
FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);
CREATE TABLE order_lines (
order_id INT NOT NULL,
line_number INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL CHECK (quantity > 0),
PRIMARY KEY (order_id, line_number),
FOREIGN KEY (order_id) REFERENCES orders(order_id),
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
属性の定義とデータ型選択
論理設計の段階で、各属性に適切なデータ型を選びます。 型の選択は、格納効率・検索性能・データ整合性に直結します。
VARCHAR vs TEXT
| 型 | 特徴 | 使いどころ |
|---|---|---|
VARCHAR(n) |
最大長 n を指定。長さ制約が入力検証の一部になる | 顧客名、電話番号、商品コードなど、長さの上限が明確なもの |
TEXT |
長さ制限なし(DBMS 上限まで)。インデックスに制限がある場合あり | 備考、説明文、メモなど、長さが予測しにくい自由入力 |
INT vs BIGINT
| 型 | 範囲 | 使いどころ |
|---|---|---|
INT |
約 -21億 〜 +21億(4バイト) | 多くのID列、数量、フラグなど。一般的な用途にはこれで十分 |
BIGINT |
約 -922京 〜 +922京(8バイト) | 大量レコードの連番ID(SNS投稿IDなど)、ミリ秒単位のタイムスタンプ値 |
NUMERIC vs FLOAT
| 型 | 特徴 | 使いどころ |
|---|---|---|
NUMERIC(p,s) / DECIMAL |
固定小数点。誤差なし | 金額、税率、単価など、正確さが必要な数値 |
FLOAT / DOUBLE |
浮動小数点。計算速度は速いが丸め誤差が生じる | 科学計算、統計値、座標など、わずかな誤差が許容される場面 |
金額に FLOAT を使わないでください。
0.1 + 0.2 = 0.30000000000000004 のような丸め誤差が発生し、
請求金額が1円ずれるなどの問題が起きます。金額は必ず NUMERIC を使います。
DATE vs TIMESTAMP
| 型 | 格納内容 | 使いどころ |
|---|---|---|
DATE |
年月日のみ(2026-02-16) | 受注日、生年月日、有効期限日など、時刻が不要な日付 |
TIMESTAMP |
年月日 + 時分秒(2026-02-16 14:30:00) | 作成日時、更新日時、ログ記録など、時刻精度が必要な場面 |
タイムゾーンを扱うシステムでは TIMESTAMP WITH TIME ZONE (PostgreSQL)
や UTC 変換ルールを設計段階で決めておくと、後から混乱しません。
命名規約
テーブル名・列名の命名は、チーム全体の可読性と保守性に影響します。 プロジェクト初期に規約を定め、レビューで一貫性を保ちましょう。
| ルール | 良い例 | 悪い例 | 理由 |
|---|---|---|---|
| スネークケースを使う | order_date |
orderDate, OrderDate |
SQL は大文字小文字を区別しない DBMS が多く、スネークケースが最も安全 |
| 英語名を使う | customers |
kokyaku |
ローマ字は非日本語話者に意味不明。英語なら共通理解できる |
| 略語を避ける | customer_name |
cust_nm |
略語はチーム内でも解釈がばらつく。可読性を優先する |
| テーブル名は複数形 | products |
product |
テーブルは行の集合であるため、複数形が自然(チーム規約による) |
| 予約語を避ける | order_status |
order, user, group |
SQL 予約語と衝突すると、常に引用符が必要になり事故の元 |
| 主キー列名を統一 | customer_id |
id, cid |
JOIN 時にどのテーブルの ID か一目でわかる |
| BOOLEAN 列は状態を示す | is_deleted, is_active |
deleted, flag |
is_ 接頭辞で BOOLEAN であることが明確になる |
よく使われる SQL 予約語(テーブル名・列名に使わないこと)
| 予約語 | 代替案 |
|---|---|
user | users, app_user, accounts |
order | orders, purchase_order |
group | groups, user_group |
table | 使わない(テーブル名としてはまず不適切) |
column | 使わない |
date | order_date, created_date |
status | order_status, account_status |
論理削除と物理削除
レコードを「削除」する方法は2つあります。それぞれ利点と欠点があり、業務要件に応じて使い分けます。
比較表
| 観点 | 物理削除(DELETE) | 論理削除(フラグ更新) |
|---|---|---|
| 実装 | DELETE FROM orders WHERE ... |
UPDATE orders SET is_deleted = TRUE WHERE ... |
| データの復元 | 不可能(バックアップからのみ) | フラグを戻すだけで復元可能 |
| ストレージ | 削除後に解放される | 削除済みデータも残り続ける |
| 検索クエリ | 条件追加は不要 | 全ての SELECT に WHERE is_deleted = FALSE が必要 |
| ユニーク制約 | 問題なし | 削除済みレコードと衝突する可能性がある(部分インデックスで対処) |
| 外部キー整合性 | 参照先が消えると FK エラー(CASCADE 設定が必要) | 参照先は残るので FK エラーは起きない |
| 監査・追跡 | 削除ログを別途記録しないと追跡できない | 誰がいつ削除したか(deleted_at)を記録しやすい |
判断基準表
| 条件 | 推奨 |
|---|---|
| 法令で一定期間の保存が義務づけられている | 論理削除 |
| 削除後に「元に戻したい」という業務要件がある | 論理削除 |
| 削除の監査ログが必須(誰が・いつ削除したか) | 論理削除 |
| 個人情報保護法等により完全消去が求められる | 物理削除 |
| 大量データでストレージ圧迫が懸念される | 物理削除(またはアーカイブテーブルへ移動) |
| ユニーク制約が複雑で論理削除との両立が困難 | 物理削除 |
| 参照されるマスタデータ(商品、カテゴリなど) | 論理削除(既存トランザクションからの参照を壊さない) |
論理削除を採用する場合は、is_deleted BOOLEAN DEFAULT FALSE と
deleted_at TIMESTAMP NULL の両方を持たせると、
削除フラグでの絞り込みと削除日時での監査の両方に対応できます。
-- 論理削除カラムの追加例
ALTER TABLE products
ADD COLUMN is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN deleted_at TIMESTAMP NULL;
-- 有効データだけを返すビュー
CREATE VIEW active_products AS
SELECT * FROM products WHERE is_deleted = FALSE;
履歴管理の設計パターン
「過去のある時点でこの値はどうだったか」を知りたい場合、履歴管理の設計が必要です。 代表的な2つのパターンを紹介します。
パターン1: 有効開始日/終了日パターン
同じテーブル内に有効期間を持たせる方式です。SCD Type 2(Slowly Changing Dimension Type 2)とも呼ばれます。
CREATE TABLE product_prices (
product_id INT NOT NULL,
valid_from DATE NOT NULL,
valid_to DATE, -- NULL = 現在有効
unit_price NUMERIC(10,2) NOT NULL,
PRIMARY KEY (product_id, valid_from),
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
-- 現在有効な単価を取得
SELECT product_id, unit_price
FROM product_prices
WHERE valid_to IS NULL;
-- 2025年6月1日時点の単価を取得
SELECT product_id, unit_price
FROM product_prices
WHERE valid_from <= '2025-06-01'
AND (valid_to IS NULL OR valid_to > '2025-06-01');
| product_id | valid_from | valid_to | unit_price |
|---|---|---|---|
| 1001 | 2024-01-01 | 2025-03-31 | 1200.00 |
| 1001 | 2025-04-01 | NULL | 1350.00 |
valid_to が NULL のレコードが「現在有効」を示します。
新しい単価に変更する際は、既存行の valid_to を設定し、新しい行を INSERT します。
| メリット | デメリット |
|---|---|
| 任意時点の値を1つのテーブルで検索できる | 「現在値」の取得に WHERE 条件が増える |
| テーブル数が増えない | UPDATE ではなく INSERT + 旧行の valid_to 更新が必要 |
| 時系列分析がしやすい | 主キーが複合キーになり、JOIN がやや複雑になる |
パターン2: 履歴テーブル分離パターン
現在値を保持するテーブルと、変更履歴を保持するテーブルを分ける方式です。
-- 現在値テーブル(常に最新のみ)
CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name VARCHAR(100) NOT NULL,
unit_price NUMERIC(10,2) NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- 履歴テーブル(変更のたびに INSERT)
CREATE TABLE products_history (
history_id INT PRIMARY KEY,
product_id INT NOT NULL,
product_name VARCHAR(100) NOT NULL,
unit_price NUMERIC(10,2) NOT NULL,
changed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
changed_by VARCHAR(50),
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
-- 現在値の取得は単純な SELECT
SELECT * FROM products WHERE product_id = 1001;
-- 変更履歴の確認
SELECT * FROM products_history
WHERE product_id = 1001
ORDER BY changed_at DESC;
| メリット | デメリット |
|---|---|
| 現在値の取得が単純(条件なしの SELECT) | テーブル数が2倍になる |
| 既存クエリへの影響が少ない | 更新時に現テーブルの UPDATE + 履歴テーブルへの INSERT が必要 |
| 履歴テーブルだけアーカイブ・パージできる | トリガーやアプリ側で二重書き込みの仕組みが必要 |
使い分けの目安: 「任意時点の状態を頻繁に参照する」なら有効開始日/終了日パターン、 「現在値を高速に取得しつつ、変更の記録も残したい」なら履歴テーブル分離パターンが向いています。
設計の進め方まとめ
以下のチェックリストを使い、設計の抜け漏れを防ぎましょう。
概念設計チェックポイント
- 業務要件の文章から、エンティティ(名詞)を洗い出したか
- 各エンティティがリソース系かイベント系かを分類したか
- エンティティ間の関連と多重度(1:1, 1:N, M:N)を定義したか
- M:N は中間テーブルで解消したか
- 概念 ER 図を描き、業務担当者と合意したか
論理設計チェックポイント
- 全テーブルに適切な主キーがあるか
- 外部キーで関連を明示しているか
- 第3正規形(3NF)まで正規化されているか(正規化ページ参照)
- データ型の選択は適切か(金額に FLOAT を使っていないか、など)
- NOT NULL 制約を適切に設定しているか
- 命名規約に沿っているか(スネークケース、英語名、予約語回避)
- 導出値をテーブルに持つ場合、整合性の維持方法を明確にしたか
物理設計チェックポイント
- WHERE 句や JOIN でよく使う列にインデックスを設計したか
- 想定レコード数に対してデータ型のサイズは十分か(INT vs BIGINT)
- 論理削除と物理削除の方針を決めたか
- 履歴管理が必要なテーブルの設計パターンを選択したか
- 文字コード(UTF-8 推奨)と照合順序を決めたか
- バックアップ・リストアの計画があるか
設計は一度で完成するものではありません。 プロトタイプで実際にデータを入れて SQL を書き、使いにくい箇所があれば設計にフィードバックする反復が大切です。