Use Cases (更新: 2026/6/2)

Claude Codeでデータベース設計をレビューする実践ガイド

Claude CodeをDB設計レビューに使う実践手順。制約、索引、マイグレーション、失敗例まで解説。

Claude Codeでデータベース設計をレビューする実践ガイド

データベース設計は、テーブル名を決めるだけの作業ではありません。どの情報をどの表に置くか、必須にする列は何か、同じ値を二重登録させない制約はあるか、将来の検索や請求処理で遅くならないかまで考える必要があります。ここを曖昧にしたまま実装を進めると、画面は動いているのに重複データ、削除できない親子関係、戻せないマイグレーションが残ります。

Claude Codeは、この設計を一人で抱え込まないためのレビュー相手として使えます。ただし、Claude Codeを「正解を出す神託」として扱うのは危険です。AIが出したスキーマは、公式ドキュメント、実データ、クエリ計画、チームの運用ルールで検証してから採用します。この記事では、初心者にも分かるように、テーブル、列、主キー、外部キー、ユニーク制約、インデックス、マイグレーション、ロールバックを整理し、Claude Codeに何を見てもらうべきかを具体化します。

関連する実装手順はDBマイグレーション自動化Prisma ORM活用も合わせて読むとつながります。制約とインデックスの判断は、必ずPostgreSQLの制約ドキュメントPostgreSQLのインデックスドキュメントで確認してください。監査ログや個人情報を扱う場合はOWASP Top 10の観点も外せません。

まず押さえるDB設計の基本

初心者が最初につまずくのは、用語が抽象的に見えることです。ここでは、SaaSのユーザー管理を例にして見ます。

用語平易な説明実務での判断
テーブル同じ種類のデータを入れる表users, teams, projectsのように責務を分ける
カラムテーブルの1項目email, created_at, statusなど。必須ならNOT NULL
主キー1行を一意に識別する番号やIDほとんどのテーブルにidを置く。中間テーブルでは複合主キーも使う
外部キー別テーブルの行を参照する約束projects.team_idteams.idを指す、など
ユニーク制約同じ値の重複を禁止する約束メールアドレス、チーム内slug、決済顧客IDに使う
インデックス検索を速くする目次WHERE, JOIN, ORDER BYで頻出する列に置く
マイグレーションスキーマ変更の手順書ALTER TABLEをPRでレビューし、適用順を残す
ロールバック変更を戻す手順破壊的変更ほど「戻せるか」を先に確認する

ここで大事なのは、制約とインデックスを後回しにしないことです。アプリ側のバリデーションだけで重複登録を防ごうとしても、同時実行やバッチ処理で破られることがあります。データベース側に最後の砦を置くから、バグが起きても壊れ方を小さくできます。

Claude Codeに任せる範囲を決める

Claude Codeに向いているのは、設計案のたたき台作成、レビュー観点の列挙、マイグレーションのリスク洗い出し、テストSQLの作成です。一方で、最終判断は人間が持ちます。たとえば「display_nameNULL許可にするか」は、画面仕様、既存データ、CSVインポート、サポート運用まで見ないと決められません。

プロンプトでは、Claude Codeに「実装者」ではなく「レビュー担当」として振る舞わせます。次のように、禁止事項と確認観点を先に渡すと、出力がかなり安定します。

claude -p "docs/database/schema.sql と migrations/*.sql を読んでください。
あなたはDB設計のレビュー担当です。正解を断定せず、根拠と確認方法を添えてください。

確認する観点:
- 主キー、外部キー、ユニーク制約、NOT NULL が不足していないか
- status や role が自由文字列になりすぎていないか
- WHERE / JOIN / ORDER BY に必要なインデックスがあるか
- 破壊的マイグレーションとロールバック不能な変更がないか
- 監査ログや検索用テーブルにメールアドレスなどのPIIが漏れていないか
- N+1クエリを誘発しそうな関連設計がないか

出力は Findings first にしてください。必要なら最小差分の修正案も出してください。"

この指示の狙いは、Claude Codeに「良さそうなスキーマ」を作らせることではありません。抜け漏れのあるレビュー項目を明文化し、人間が判断できる形に戻すことです。

4つのユースケースで考える

この記事のサンプルでは、次の4つを同時に扱います。単純なusersだけの例より、実務で起きる失敗が見えやすいからです。

1つ目は、SaaSのユーザー、チーム、プロジェクト管理です。ユーザーは複数チームに参加でき、チーム内にプロジェクトがあります。この場合、projects.slugは全体で一意ではなく「チーム内で一意」にするのが自然です。

2つ目は、監査ログです。誰が、いつ、どの対象に、何をしたかを残します。ただし、便利だからといってメールアドレスや住所をそのままmetadataに入れると、ログ保管期間、権限、削除要求で苦しくなります。

3つ目は、請求とサブスクリプションです。決済プロバイダの顧客IDは重複させてはいけません。また、同じチームに有効なサブスクリプションが複数あると、請求や権限判定が壊れます。

4つ目は、検索やレポート用のテーブルです。検索を毎回複雑なJOINで処理すると遅くなりやすいので、読み取り用のテーブルを別に持つ判断もあります。ただし、元データとの同期方法を設計しないと、古い検索結果を出し続けます。

コピーして動かせるPostgreSQL DDL

次のSQLは、ローカルのPostgreSQLにそのまま流せるサンプルです。既存データは消さないよう、専用スキーマclaude_db_design_demoだけを作り直します。

DROP SCHEMA IF EXISTS claude_db_design_demo CASCADE;
CREATE SCHEMA claude_db_design_demo;
SET search_path = claude_db_design_demo;

CREATE TABLE users (
  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  email TEXT NOT NULL,
  display_name TEXT NOT NULL,
  password_hash TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  CONSTRAINT users_email_unique UNIQUE (email),
  CONSTRAINT users_email_has_at CHECK (position('@' in email) > 1)
);

CREATE TABLE teams (
  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name TEXT NOT NULL,
  slug TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  CONSTRAINT teams_slug_unique UNIQUE (slug)
);

CREATE TABLE team_members (
  team_id BIGINT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
  user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'member')),
  joined_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  PRIMARY KEY (team_id, user_id)
);

CREATE TABLE projects (
  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  team_id BIGINT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  slug TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived')),
  created_by BIGINT NOT NULL REFERENCES users(id),
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  CONSTRAINT projects_team_slug_unique UNIQUE (team_id, slug)
);

CREATE TABLE audit_logs (
  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  team_id BIGINT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
  actor_user_id BIGINT REFERENCES users(id) ON DELETE SET NULL,
  action TEXT NOT NULL,
  entity_type TEXT NOT NULL,
  entity_id TEXT NOT NULL,
  metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  CONSTRAINT audit_action_not_blank CHECK (length(action) > 0),
  CONSTRAINT audit_metadata_no_email CHECK (
    metadata::text !~* '[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}'
  )
);

CREATE TABLE subscriptions (
  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  team_id BIGINT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
  provider TEXT NOT NULL CHECK (provider IN ('stripe', 'manual')),
  provider_customer_id TEXT NOT NULL,
  plan_code TEXT NOT NULL CHECK (plan_code IN ('free', 'pro', 'business')),
  status TEXT NOT NULL CHECK (status IN ('trialing', 'active', 'past_due', 'canceled')),
  current_period_end DATE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  CONSTRAINT subscriptions_provider_customer_unique UNIQUE (provider, provider_customer_id)
);

CREATE TABLE project_search_documents (
  project_id BIGINT PRIMARY KEY REFERENCES projects(id) ON DELETE CASCADE,
  team_id BIGINT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
  title TEXT NOT NULL,
  search_text TEXT NOT NULL,
  refreshed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE UNIQUE INDEX subscriptions_one_open_per_team
  ON subscriptions(team_id)
  WHERE status IN ('trialing', 'active', 'past_due');

CREATE INDEX idx_team_members_user_id ON team_members(user_id);
CREATE INDEX idx_projects_team_status_created ON projects(team_id, status, created_at DESC);
CREATE INDEX idx_audit_logs_team_created ON audit_logs(team_id, created_at DESC);
CREATE INDEX idx_audit_logs_entity ON audit_logs(entity_type, entity_id);
CREATE INDEX idx_project_search_documents_search
  ON project_search_documents USING GIN (to_tsvector('simple', search_text));

projects_team_slug_uniqueは「チーム内で同じslugを禁止する」制約です。subscriptions_one_open_per_teamは、解約済みの履歴を残しながら、進行中の契約だけはチームごとに1つへ制限します。これはユニーク制約ではなく部分ユニークインデックスですが、実務ではかなり役立ちます。

シードデータと制約テスト

DDLだけ見ても、設計が本当に効いているかは分かりません。最低限のシードデータと、失敗すべきINSERTを用意します。Claude Codeには、ここまで含めて「成功するケース」と「失敗するケース」を作らせるのが有効です。

SET search_path = claude_db_design_demo;

INSERT INTO users (email, display_name, password_hash) VALUES
  ('[email protected]', 'Masa', '$2b$demo-hash-1'),
  ('[email protected]', 'Aiko', '$2b$demo-hash-2'),
  ('[email protected]', 'Ren', '$2b$demo-hash-3');

INSERT INTO teams (name, slug) VALUES
  ('ClaudeCodeLab', 'clab'),
  ('Northstar Apps', 'northstar');

INSERT INTO team_members (team_id, user_id, role) VALUES
  (1, 1, 'owner'),
  (1, 2, 'admin'),
  (2, 3, 'owner');

INSERT INTO projects (team_id, name, slug, status, created_by) VALUES
  (1, 'Schema Review', 'schema-review', 'active', 1),
  (1, 'Billing Console', 'billing-console', 'active', 2),
  (2, 'Customer Portal', 'customer-portal', 'active', 3);

INSERT INTO audit_logs (team_id, actor_user_id, action, entity_type, entity_id, metadata) VALUES
  (1, 1, 'project.created', 'project', '1', '{"project_slug":"schema-review"}'::jsonb),
  (1, 2, 'billing.plan_changed', 'subscription', '1', '{"from":"free","to":"pro"}'::jsonb);

INSERT INTO subscriptions (team_id, provider, provider_customer_id, plan_code, status, current_period_end) VALUES
  (1, 'stripe', 'cus_demo_1', 'pro', 'active', DATE '2026-07-31'),
  (2, 'manual', 'manual_northstar_1', 'free', 'canceled', NULL);

INSERT INTO project_search_documents (project_id, team_id, title, search_text)
SELECT
  id,
  team_id,
  name,
  name || ' project notes for database design, billing, audit, and reporting.'
FROM projects;
SET search_path = claude_db_design_demo;

DO $$
BEGIN
  INSERT INTO users (email, display_name, password_hash)
  VALUES ('[email protected]', 'Duplicate Masa', '$2b$demo-hash-x');
EXCEPTION WHEN unique_violation THEN
  RAISE NOTICE 'OK duplicate email blocked: %', SQLERRM;
END $$;

DO $$
BEGIN
  INSERT INTO projects (team_id, name, slug, created_by)
  VALUES (1, NULL, 'bad-null', 1);
EXCEPTION WHEN not_null_violation THEN
  RAISE NOTICE 'OK NULL project name blocked: %', SQLERRM;
END $$;

DO $$
BEGIN
  INSERT INTO team_members (team_id, user_id, role)
  VALUES (999, 1, 'member');
EXCEPTION WHEN foreign_key_violation THEN
  RAISE NOTICE 'OK missing team blocked: %', SQLERRM;
END $$;

DO $$
BEGIN
  INSERT INTO projects (team_id, name, slug, created_by)
  VALUES (1, 'Duplicate Slug', 'schema-review', 1);
EXCEPTION WHEN unique_violation THEN
  RAISE NOTICE 'OK duplicate team slug blocked: %', SQLERRM;
END $$;

DO $$
BEGIN
  INSERT INTO audit_logs (team_id, actor_user_id, action, entity_type, entity_id, metadata)
  VALUES (1, 1, 'project.invited', 'project', '1', '{"email":"[email protected]"}'::jsonb);
EXCEPTION WHEN check_violation THEN
  RAISE NOTICE 'OK email in audit metadata blocked: %', SQLERRM;
END $$;

DO $$
BEGIN
  INSERT INTO subscriptions (team_id, provider, provider_customer_id, plan_code, status, current_period_end)
  VALUES (1, 'stripe', 'cus_demo_2', 'business', 'active', DATE '2026-08-31');
EXCEPTION WHEN unique_violation THEN
  RAISE NOTICE 'OK second open subscription blocked: %', SQLERRM;
END $$;

SELECT p.id, p.name, count(a.id) AS audit_events
FROM projects p
LEFT JOIN audit_logs a
  ON a.entity_type = 'project'
 AND a.entity_id = p.id::text
WHERE p.team_id = 1
GROUP BY p.id, p.name
ORDER BY audit_events DESC, p.id;

SELECT project_id, title
FROM project_search_documents
WHERE to_tsvector('simple', search_text) @@ plainto_tsquery('simple', 'billing');

EXPLAIN (COSTS OFF)
SELECT *
FROM audit_logs
WHERE team_id = 1
ORDER BY created_at DESC
LIMIT 20;

ここで重要なのは、失敗テストを「失敗してよかった」と確認することです。ユニーク制約に引っかかる、NOT NULLで止まる、外部キーで止まる、監査ログのPIIが止まる。これらは面倒なエラーではなく、設計がデータを守っている証拠です。

Claude Codeにレビューさせる観点

上のSQLをClaude Codeに渡すなら、次のような観点でレビューさせます。

claude -p "このPostgreSQL DDLをレビューしてください。
初心者にも分かる言葉で、次の順に指摘してください。

1. データ不整合を防ぐ制約の不足
2. NULL許可が危険なカラム
3. ユニーク制約または部分ユニークインデックスが必要な箇所
4. JOIN、一覧、検索で不足しているインデックス
5. 破壊的マイグレーションになりそうな変更
6. 監査ログや検索テーブルへのPII漏えい
7. N+1クエリを誘発しやすいAPI設計

各指摘には、なぜ問題か、どう直すか、どのSQLで検証するかを添えてください。"

レビュー結果を読むときは、必ず根拠を確認します。Claude Codeが「この列にインデックスを追加」と言っても、実際にその列で絞り込みや並び替えをしていないなら、不要なインデックスかもしれません。インデックスは読み取りを速くする一方で、書き込みを少し重くし、ストレージも使います。

よくある失敗例

まず多いのは、ユニーク制約の不足です。メールアドレス、OAuthプロバイダのアカウントID、決済顧客ID、チーム内slugをアプリ側だけでチェックしていると、同時リクエストで重複が入ります。Claude Codeには「どの値が業務上1つだけであるべきか」を列挙させます。

次に、NULL許可の甘さです。projects.nameteam_members.roleNULLになると、一覧画面、権限判定、通知テンプレートが壊れます。反対に、退会済みユーザーのdeleted_atのように、空であることに意味がある列もあります。NULLは悪ではありませんが、意味を説明できないNULLは危険です。

破壊的マイグレーションも頻出します。DROP COLUMN、型変更、NOT NULL追加、巨大テーブルへのデフォルト付き列追加は、ロックやデータ消失につながります。Claude Codeには「本番データが1000万行ある前提で、安全な段階分割案を出して」と依頼すると、雑なALTER TABLEを避けやすくなります。

N+1クエリも設計段階で見つけたい問題です。プロジェクト一覧を取った後に、各プロジェクトごとに監査ログ件数を別クエリで取りに行くと、100件の一覧で101回SQLが走ります。集計が必要なら、JOINや集計テーブル、読み取り用テーブルを検討します。

最後に、監査ログへのPII漏えいです。ログは便利なので何でも入れたくなりますが、保管期間が長く、閲覧できる人も増えがちです。メールアドレス、電話番号、住所、トークン、IPアドレスを入れる必要が本当にあるかを確認し、必要ならマスキング、ハッシュ化、別権限テーブルを使います。

マイグレーションとロールバックのチェックリスト

マイグレーションは「動いたSQL」ではなく「安全に配れる変更手順」です。Claude Codeに次のMarkdownをPR本文へ入れさせると、レビューの質が上がります。

# Migration checklist: add projects.archived_at

## Design
- [ ] Why this column is needed is written in the issue or PR.
- [ ] Existing rows and backfill behavior are defined.
- [ ] NOT NULL, default value, and CHECK constraint choices are explained.
- [ ] Index impact was checked against real WHERE / ORDER BY queries.

## Safety
- [ ] This migration does not drop data in the first deploy.
- [ ] Large-table locks were considered.
- [ ] The application can run during the expand-and-contract period.
- [ ] Backfill can be retried safely.

## Up SQL
    ALTER TABLE projects ADD COLUMN archived_at TIMESTAMPTZ;
    UPDATE projects
    SET archived_at = now()
    WHERE status = 'archived' AND archived_at IS NULL;
    ALTER TABLE projects
      ADD CONSTRAINT projects_archived_at_matches_status
      CHECK (
        (status = 'archived' AND archived_at IS NOT NULL)
        OR (status = 'active' AND archived_at IS NULL)
      ) NOT VALID;
    ALTER TABLE projects VALIDATE CONSTRAINT projects_archived_at_matches_status;

## Down SQL
    ALTER TABLE projects DROP CONSTRAINT IF EXISTS projects_archived_at_matches_status;
    ALTER TABLE projects DROP COLUMN IF EXISTS archived_at;

## Verification
- [ ] Constraint test SQL was run locally.
- [ ] Read queries still use expected indexes.
- [ ] Rollback was reviewed, including data-loss notes.
- [ ] Claude Code findings were reviewed by a human owner.

このチェックリストのポイントは、ロールバックを最初から書くことです。実際には、データ削除後に完全なロールバックができない変更もあります。その場合は「戻せない」と明記し、バックアップ、段階リリース、読み取り停止、復旧手順を別に用意します。Claude Codeが生成したdown migrationをそのまま信じるのではなく、データが戻るのか、スキーマだけ戻るのかを分けて見ます。

設計を図で共有する

レビュー時は、SQLだけでなく関係図もあると会話が早くなります。MermaidのER図をClaude Codeに出させ、PR本文や設計メモに貼ると、非バックエンド担当も関係を追いやすくなります。

erDiagram
    users ||--o{ team_members : joins
    teams ||--o{ team_members : has
    teams ||--o{ projects : owns
    users ||--o{ projects : creates
    teams ||--o{ audit_logs : records
    users ||--o{ audit_logs : acts
    teams ||--o{ subscriptions : pays
    projects ||--|| project_search_documents : indexes

図はあくまで会話の補助です。最終的にはDDL、マイグレーション、テスト、実クエリを見ます。図がきれいでも、ユニーク制約がない設計は壊れます。

CTA: チーム導入ではレビュー基準を先に作る

Claude CodeをDB設計に入れるなら、個人の便利技で終わらせず、レビュー基準をチームの型にしてください。CLAUDE.mdに「本番DBへ直接接続しない」「破壊的マイグレーションは段階分割する」「制約テストを必ず書く」「PIIをログに入れない」と書くだけでも、事故の確率は下がります。

ClaudeCodeLabでは、Claude Codeの導入、CLAUDE.md整備、DBマイグレーションのレビュー手順、PRテンプレート、チーム研修まで実務に合わせて支援しています。既存サービスのスキーマを安全に見直したい場合は、Claude Code研修・導入相談から相談してください。個人で型を増やしたい場合は、無料チートシート商品一覧も使えます。

まとめ

データベース設計では、テーブルと列を作るだけでなく、主キー、外部キー、ユニーク制約、NOT NULL、インデックス、マイグレーション、ロールバックを一つの流れで見る必要があります。Claude Codeは、その全体像をレビュー観点へ分解する相手として有効です。

ただし、最終判断は必ず人間が持ちます。Claude Codeに設計案、失敗テスト、チェックリスト、ER図を作らせ、公式ドキュメントと実クエリで検証する。この順番にすると、AIの速度を使いながら、データを守る設計に近づけます。

この記事で紹介した内容をMasaが実際に試した結果、いちばん効果があったのは「失敗すべきINSERT」を最初から用意することでした。Claude Codeにレビューだけ頼むより、重複メール、NULL、外部キー違反、監査ログのPII混入をSQLで確認したほうが、設計の穴に早く気づけました。DB設計では、きれいな説明より、壊れ方を先に試す習慣が効きます。

#claude-code #データベース #prisma #sql #設計
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。