mcpField
D
database-schema-design

database-schema-design

github.com/mohitagw15856データ・データベース1.5k50 回インストール1 週間前に更新
リポジトリ

database-schema-design について

database-schema-design は、データベース設計文書を作成するエンジニアやチーム向けのスキルです。テーブル定義に加え、エンティティの関係、クエリパターン、インデックス方針、設計上の判断を記録します。

主な機能

  1. 01ドメインのエンティティと関係を整理
  2. 02テーブル定義と図を作成
  3. 03クエリパターンに基づく索引設計
  4. 04アクセスパターンと正規化を分析
  5. 05移行と自動化の方針を記録

活用シーン

  1. 01新しいサービスのデータベース設計
  2. 02頻出クエリ向けのインデックス計画
  3. 03エンジニアレビュー用の設計文書作成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mohitagw15856/pm-claude-skills --skill database-schema-design

インストール後、Claude Code で/database-schema-designを入力して読み込まれたか確認します。

詳細

データベーススキーマ設計スキル

指定されたドメインについて、完全なデータベーススキーマ設計ドキュメントを作成します。スキーマドキュメントは単なるテーブルの一覧ではありません。何をモデル化したか、エンティティをどのように関連付けたか、スキーマをどのクエリ向けに最適化したか、どのようなトレードオフを選んだかを記録するものです。

優れたスキーマ設計ドキュメントがあれば、エンジニアはデータモデルを理解し、正しくクエリし、安全に拡張して、問題を起こさずにマイグレーションを作成できます。

必要な入力

まだ提示されていない場合は、以下の情報を尋ねてください。

  • ドメインの説明 — システムの機能、およびモデル化するビジネスオブジェクト
  • エンティティとリレーションシップ — ドメインの主要な要素とその関係(例:「ユーザーは複数の注文を持つ。注文は複数の注文明細を持つ。注文明細は商品を参照する」)
  • 想定されるクエリパターン — 最も重要な読み取りおよび書き込みクエリ(例:「ユーザーのすべての注文を日付順に取得する」「SKU から商品を検索する」)
  • データベースエンジン — PostgreSQL、MySQL、SQLite、CockroachDB など。DDL 構文や利用可能な型に影響します
  • 想定されるデータ量 — おおよその行数、増加率、パーティショニングの必要性
  • 制約 — 遵守すべき既存の規約、命名規則、マイグレーション上の制約

出力形式


データベーススキーマ設計:[ドメイン/サービス名]

サービス: [名前] | チーム: [チーム名] 作成者: [名前] | レビュー担当者: [名前] 日付: [日付] | データベースエンジン: [PostgreSQL X.X/MySQL X.X/など] ステータス: [ドラフト/レビュー済み/承認済み]


1. 概要

[モデル化するドメイン、このスキーマの対象範囲、および主要な設計方針を説明する2~3文(例:「このスキーマは書き込みの単純さよりも、顧客向け API の読み取りパフォーマンスを優先します」または「将来的なマルチテナントへの移行を想定して設計されています」)]

対象範囲:

  • [エンティティまたはサブシステム]
  • [エンティティまたはサブシステム]

対象外:

  • [例:分析/レポート用テーブル — 別のスキーマで管理]
  • [例:監査ログテーブル — 別の設計ドキュメントで扱う]

2. エンティティ・リレーションシップ図

┌───────────────────┐         ┌───────────────────────┐
│      users        │         │       organisations    │
│─────────────────  │         │─────────────────────── │
│ id (PK)           │    ┌───▶│ id (PK)                │
│ org_id (FK)  ─────┼────┘    │ name                   │
│ email             │         │ plan                   │
│ display_name      │         │ created_at             │
│ created_at        │         └───────────────────────┘
│ updated_at        │
└─────────┬─────────┘
          │ 1
          │
          │ N
┌─────────▼─────────┐         ┌───────────────────────┐
│      [table_a]    │         │      [table_b]         │
│─────────────────  │         │─────────────────────── │
│ id (PK)           │    N    │ id (PK)                │
│ user_id (FK) ─────┼────────▶│ [table_a]_id (FK)      │
│ [field]           │    │    │ [field]                │
│ [field]           │    │    │ [field]                │
│ created_at        │         │ created_at             │
└───────────────────┘         └───────────────────────┘

リレーションシップの概要:

エンティティ Aリレーションシップエンティティ B備考
organisations複数を持つusers1つの組織は複数のユーザーを持てます
users複数を持つ[table_a]ユーザーの削除時に論理削除
[table_a]複数を持つ[table_b]カスケード削除
[table_b]に属する[table_a]NULL 不可の FK
[table_c]多対多([join_table] 経由)[table_d]メタデータを持つ結合テーブル

3. テーブル定義

organisations

[このテーブルに格納する内容と、ドメインにおける役割を説明する1文。]

CREATE TABLE organisations (
    id          UUID            PRIMARY KEY DEFAULT gen_random_uuid(),
    name        VARCHAR(255)    NOT NULL,
    slug        VARCHAR(100)    NOT NULL UNIQUE,
    plan        VARCHAR(50)     NOT NULL DEFAULT 'free'
                                CHECK (plan IN ('free', 'pro', 'enterprise')),
    settings    JSONB           NOT NULL DEFAULT '{}',
    created_at  TIMESTAMPTZ     NOT NULL DEFAULT now(),
    updated_at  TIMESTAMPTZ     NOT NULL DEFAULT now()
);
カラム型NULL 可デフォルト備考
idUUIDいいえgen_random_uuid()サロゲート PK — 分散利用では serial より UUID を推奨
nameVARCHAR(255)いいえ—表示名。ユニークではありません
slugVARCHAR(100)いいえ—URL で安全に使用できる識別子。すべての組織で一意
planVARCHAR(50)いいえ'free'CHECK により既知の値に制限
settingsJSONBいいえ{}柔軟な設定用。クエリ対象のフィールドには使用しないでください
created_atTIMESTAMPTZいいえnow()TIMESTAMP ではなく、必ず TIMESTAMPTZ を使用
updated_atTIMESTAMPTZいいえnow()トリガーで更新(下記参照)

users

[このテーブルに格納する内容を説明する1文。]

CREATE TABLE users (
    id              UUID            PRIMARY KEY DEFAULT gen_random_uuid(),
    org_id          UUID            NOT NULL REFERENCES organisations(id)
                                    ON DELETE RESTRICT,
    email           VARCHAR(254)    NOT NULL,
    display_name    VARCHAR(255)    NOT NULL DEFAULT '',
    role            VARCHAR(50)     NOT NULL DEFAULT 'member'
                                    CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
    email_verified  BOOLEAN         NOT NULL DEFAULT false,
    deleted_at      TIMESTAMPTZ     NULL,
    created_at      TIMESTAMPTZ     NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ     NOT NULL DEFAULT now(),

    CONSTRAINT users_email_org_unique UNIQUE (email, org_id)
);
カラム型NULL 可デフォルト備考
idUUIDいいえgen_random_uuid()—
org_idUUIDいいえ—organisations への FK。RESTRICT により孤立を防止
emailVARCHAR(254)いいえ—RFC 5321 の最大長。組織ごとに一意(全体では一意ではありません)
roleVARCHAR(50)いいえ'member'アプリケーションレベルの RBAC
deleted_atTIMESTAMPTZはいNULL論理削除。NULL = 有効

論理削除ポリシー: deleted_at IS NOT NULL の行は削除済みとみなします。削除済みのレコードを明示的に取得する場合を除き、すべてのアプリケーションクエリで WHERE deleted_at IS NULL による絞り込みを必ず行ってください。これを徹底するために、ビューまたは ORM のスコープを使用してください。

[table_a]

[このテーブルがモデル化する内容の説明。]

CREATE TABLE [table_a] (
    id          UUID            PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id     UUID            NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    [field_1]   VARCHAR(255)    NOT NULL,
    [field_2]   TEXT            NULL,
    [field_3]   INTEGER         NOT NULL DEFAULT 0 CHECK ([field_3] >= 0),
    status      VARCHAR(50)     NOT NULL DEFAULT 'pending'
                                CHECK (status IN ('pending', 'active', 'archived')),
    metadata    JSONB           NOT NULL DEFAULT '{}',
    created_at  TIMESTAMPTZ     NOT NULL DEFAULT now(),
    updated_at  TIMESTAMPTZ     NOT NULL DEFAULT now()
);
列型NULL 許容備考
user_idUUIDいいえCASCADE 削除 — ユーザーが削除されると、そのユーザーの [table_a] の行も削除されます
[field_1]VARCHAR(255)いいえ[長さ制約の理由]
statusVARCHAR(50)いいえステートマシン:pending → active → archived(ほかの遷移はありません)
metadataJSONBいいえ[保存される内容と、型付きの列にしない理由]

[join_table] (多対多)

[このテーブルが表す関係の説明。]

CREATE TABLE [join_table] (
    [table_c]_id    UUID        NOT NULL REFERENCES [table_c](id) ON DELETE CASCADE,
    [table_d]_id    UUID        NOT NULL REFERENCES [table_d](id) ON DELETE CASCADE,
    granted_by      UUID        NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
    granted_at      TIMESTAMPTZ NOT NULL DEFAULT now(),

    PRIMARY KEY ([table_c]_id, [table_d]_id)
);

複合主キーを使用する理由: [table_c]_id + [table_d]_id の組み合わせは自然キーです。各関連付けは一意であり、別個のインデックスを作成しなくても、主キーが一意性制約を兼ねます。


4. インデックス戦略

各テーブルについて、作成するインデックスとその理由を定義します。対象とするクエリも記載してください。

テーブルインデックス名列種類対象クエリ備考
usersusers_org_id_idx(org_id)B-treeSELECT * FROM users WHERE org_id = $1外部キーの検索用。JOIN のパフォーマンスに必要です
usersusers_email_lower_idx(lower(email))B-tree(関数)WHERE lower(email) = lower($1)大文字と小文字を区別しないメールアドレス検索
usersusers_active_by_org_idx(org_id, created_at DESC)B-treeWHERE org_id = $1 AND deleted_at IS NULL ORDER BY created_at DESC部分インデックスの候補です(下記参照)
[table_a][table_a]_user_id_status_idx(user_id, status)B-treeWHERE user_id = $1 AND status = 'active'複合インデックス — 順序が重要です
[table_a][table_a]_metadata_gin_idxmetadataGINWHERE metadata @> '{"key": "value"}'JSONB の検索頻度が高い場合にのみ追加します

部分インデックス(PostgreSQL):

-- Index only active (non-deleted) users — dramatically smaller for soft-delete tables
CREATE INDEX users_active_email_idx
    ON users (email, org_id)
    WHERE deleted_at IS NULL;

-- Index only pending items — avoids indexing the majority of rows
CREATE INDEX [table_a]_pending_idx
    ON [table_a] (user_id, created_at)
    WHERE status = 'pending';

適用するインデックス設計の原則:

  • JOIN 条件に使用する外部キーには、必ずインデックスを設定します
  • 複合インデックスは選択性の高い列を先頭にします
  • 大文字と小文字を区別しない検索には関数インデックスを使用します
  • GIN インデックスは、JSONB の包含クエリの頻度が高い場合にのみ使用します
  • 大規模なテーブルで、ステータスによる絞り込みを行うクエリには部分インデックスを使用します

5. アクセスパターン分析

このスキーマが主に対象とするクエリを記載します。それぞれについて、クエリ、使用するインデックス、注意事項を示してください。

AP-1: 組織内のすべてのアクティブユーザーを取得する(ページネーションあり)

頻度: 非常に高い — ダッシュボードの読み込みごとに呼び出されます クエリ:

SELECT id, email, display_name, role, created_at
FROM users
WHERE org_id = $1
  AND deleted_at IS NULL
ORDER BY created_at DESC
LIMIT 50 OFFSET $2;

使用するインデックス: users_active_by_org_idx (org_id, created_at DESC) 備考: 大規模なデータではキーベースのページネーション(WHERE created_at < $cursor)を使用します。OFFSET は約 10k 行を超えるとパフォーマンスが低下します。


AP-2: メールアドレスでユーザーを検索する(大文字と小文字を区別しない)

頻度: 高い — 認証の試行ごとに実行されます クエリ:

SELECT id, org_id, role, email_verified
FROM users
WHERE lower(email) = lower($1)
  AND deleted_at IS NULL;

使用するインデックス: users_email_lower_idx 備考: 同じメールアドレスが複数の組織に存在する場合、複数の行が返されます。アプリケーション側で組織のコンテキストに基づいて解決します。


AP-3: ステータス別にユーザーの [table_a] アイテムを取得する

頻度: 高い クエリ:

SELECT *
FROM [table_a]
WHERE user_id = $1
  AND status = $2
ORDER BY created_at DESC
LIMIT 25;

使用するインデックス: [table_a]_user_id_status_idx 備考: 複合インデックスが両方の絞り込み列をカバーします。user_id の方が選択性が高いため、インデックスではステータスの絞り込み列を2番目にする必要があります。


AP-4: 必要に応じてアクセスパターンを追加します

6. 正規化の判断

正規化または非正規化を行う意図的な判断と、その理由を記録します。

判断方針理由
[e.g. Organisation name on users table?]非正規化しない — 常に organisations と結合します古くなったコピーを避けます。組織名が変わることはまれで、結合のコストも低いためです
[e.g. Status history]このテーブルには含めない — 必要であれば別の [table_a]_status_history を作成しますクエリの99%では現在のステータスだけで十分です。履歴は監査用であり、アプリケーションデータではありません
[e.g. JSONB settings column on organisations]JSONB に非正規化します設定はまとめて読み取られ、フィールド単位でクエリされることはありません。スキーマの変更にマイグレーションは不要です
[e.g. Computed aggregate counts]保存しない — クエリ実行時に計算します件数は少量です。カウンター列を維持するには慎重なロック制御が必要です。インデックスを使って SELECT COUNT(*) を実行します

7. トリガーと自動化

-- Automatically update updated_at on any row modification
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = now();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Apply to all tables with updated_at
CREATE TRIGGER users_updated_at
    BEFORE UPDATE ON users
    FOR EACH ROW EXECUTE FUNCTION set_updated_at();

CREATE TRIGGER [table_a]_updated_at
    BEFORE UPDATE ON [table_a]
    FOR EACH ROW EXECUTE FUNCTION set_updated_at();

8. マイグレーションの注意事項

このスキーマを既存のシステムに導入する場合は、マイグレーションの方針を記載します。

手順説明下位互換性リスク
1organisations テーブルを作成しますあり — 追加のみ低
2users テーブルを作成しますあり — 追加のみ低
3既存のユーザーに org_id をバックフィルします二重書き込み期間が必要です中
4org_id に NOT NULL 制約を追加しますバックフィルが100%完了している必要があります中
5非推奨の列を削除します先にアプリケーションコードを更新する必要がありますアプリケーションのデプロイ後は低

バックフィル戦略: [既存データの処理方法を記述します — バッチサイズ、レート制限、検証クエリ]

ロールバック: 各マイグレーション手順は個別に元に戻せるようにします。ロールバック手順のテンプレート全体については、[database-migration-plan skill] を参照してください。


品質チェック

  • すべてのテーブルに主キーと created_at 列がある — 行の挿入順序による暗黙の並べ替えに依存しない
  • すべての外部キーに対応するインデックスがある — 結合時に全表スキャンを引き起こす外部キーのインデックス漏れがない
  • TIMESTAMP ではなく、すべて TIMESTAMPTZ 列を使う — タイムゾーンの扱いを明示する
  • 論理削除するテーブルでは、その規約とフィルターを適用する場所(ORM のスコープ、ビュー、またはクエリの標準)を記載する
  • 設計内のすべてのアクセスパターンに対応するインデックスがあるか、全表スキャンを許容できる旨を明記する
  • JSONB 列を使う理由が明確である — クエリ対象のフィールドに対する適切なスキーマ設計の代わりに使わない
  • 正規化の判断は、判断だけでなく理由も記録する
  • グリーンフィールドのスキーマではなくスキーマ変更の場合は、マイグレーションの注意事項で既存データを扱う

アンチパターン

  • クエリ対象となるフィールドについて、適切なリレーショナルスキーマ設計の代わりに JSONB 列を使わない
  • 憶測でインデックスを追加しない — すべてのインデックスに具体的なアクセスパターンに基づく根拠が必要です
  • タイムゾーンへの対応を省略しない — TIMESTAMPTZ を使い、通常の TIMESTAMP は使わない
  • 正規化の判断を記録せずに設計しない — 将来の保守担当者に必要なのは構造だけでなく、その理由です
  • アクセスパターンのセクションを省略しない — クエリパターンのないスキーマは、正しさを評価できません

トリガーフレーズの例

  • 「データベースを設計して。」
  • 「既存のスキーマを文書化して。」
  • 「エンティティとリレーションシップをモデル化して。」
  • 「テーブル構造を定義して。」
  • 「インデックス戦略を計画して。」

コメント