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 図

商品 product_id (PK) product_name unit_price 受注明細(中間テーブル) order_id (PK/FK) line_number (PK) product_id (FK) quantity 受注 order_id (PK) order_date customer_id (FK) 1 : N 1 : N 商品 M : N 受注 を 商品 1:N 受注明細 N:1 受注 に分解 中間テーブルが M:N を解消する
M:N の関連は中間テーブル(受注明細)を挟むことで、2つの 1:N に分解される。
-- 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 予約語(テーブル名・列名に使わないこと)

予約語代替案
userusers, app_user, accounts
orderorders, purchase_order
groupgroups, user_group
table使わない(テーブル名としてはまず不適切)
column使わない
dateorder_date, created_date
statusorder_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 FALSEdeleted_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_idvalid_fromvalid_tounit_price
10012024-01-012025-03-311200.00
10012025-04-01NULL1350.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 が必要
履歴テーブルだけアーカイブ・パージできるトリガーやアプリ側で二重書き込みの仕組みが必要

使い分けの目安: 「任意時点の状態を頻繁に参照する」なら有効開始日/終了日パターン、 「現在値を高速に取得しつつ、変更の記録も残したい」なら履歴テーブル分離パターンが向いています。

設計の進め方まとめ

以下のチェックリストを使い、設計の抜け漏れを防ぎましょう。

概念設計チェックポイント

論理設計チェックポイント

物理設計チェックポイント

設計は一度で完成するものではありません。 プロトタイプで実際にデータを入れて SQL を書き、使いにくい箇所があれば設計にフィードバックする反復が大切です。