mcpField
D
Database Design

Database Design

github.com/jmsktmデータ・データベース1069 回インストール40 週間前に更新
リポジトリ

Database Design について

Supabase と PostgreSQL を使って Next.js アプリを構築する開発者向けのデータベース設計スキルです。スキーマ設計、アクセス制御、マイグレーション、クエリ性能の実践方法を扱います。

主な機能

  1. 01スキーマとリレーション設計
  2. 02Row Level Security ポリシー
  3. 03インデックスとクエリ最適化
  4. 04Supabase マイグレーション
  5. 05論理削除と監査ログ
  6. 06Supabase クエリ例

活用シーン

  1. 01要件に基づくテーブル設計
  2. 02RLS による行アクセス制限
  3. 03頻出クエリの改善
  4. 04マイグレーションによるスキーマ変更

インストール

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

$npx skills add https://github.com/jmsktm/claude-settings --skill database-design

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

詳細

データベース設計スキル

Supabase と PostgreSQL を使用する Next.js アプリケーション向けに、効率的でスケーラブルなデータベーススキーマを設計するための包括的なガイドです。テーブル設計やリレーションシップから、Row Level Security ポリシー、クエリ最適化まで、このスキルでは堅牢なデータモデリングに必要なすべてを網羅します。

正規化されたスキーマを設計し、安全な RLS ポリシーを実装し、適切なインデックスでクエリパフォーマンスを最適化して、アプリケーションの成長に対応できる保守性の高いデータモデルを構築します。

基本的なワークフロー

ワークフロー 1:要件からのスキーマ設計

目的: ビジネス要件をデータベーススキーマに変換します

手順:

  1. エンティティとリレーションシップを特定します
  2. 主キーと外部キーを定義します
  3. 適切なデータ型を選択します
  4. 制約とデフォルト値を追加します
  5. パフォーマンス向上のためにインデックスを作成します

実装:

-- Correct data types
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  email TEXT NOT NULL,                    -- Use TEXT, not VARCHAR
  age INTEGER CHECK (age >= 0),
  balance DECIMAL(10, 2),                 -- For money
  is_active BOOLEAN DEFAULT true,
  metadata JSONB,                         -- Use JSONB for JSON
  created_at TIMESTAMPTZ DEFAULT NOW()    -- Use TIMESTAMPTZ
);
ワークフロー 2:一般的なリレーションシップパターン

目的: 適切なデータベースリレーションシップを実装します

1 対多:

CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  email TEXT UNIQUE NOT NULL,
  name TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE posts (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  title TEXT NOT NULL,
  content TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_posts_user_id ON posts(user_id);

多対多:

CREATE TABLE students (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  name TEXT NOT NULL
);

CREATE TABLE courses (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  name TEXT NOT NULL
);

CREATE TABLE enrollments (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  student_id UUID NOT NULL REFERENCES students(id) ON DELETE CASCADE,
  course_id UUID NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
  enrolled_at TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE(student_id, course_id)
);

CREATE INDEX idx_enrollments_student ON enrollments(student_id);
CREATE INDEX idx_enrollments_course ON enrollments(course_id);

自己参照(ツリー):

CREATE TABLE comments (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  parent_id UUID REFERENCES comments(id) ON DELETE CASCADE,
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  content TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_comments_parent ON comments(parent_id);
ワークフロー 3:Row Level Security(RLS)

目的: データベースレベルでデータアクセスを保護します

実装:

-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Users can only read their own data
CREATE POLICY "Users can read own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);

-- Users can insert their own data
CREATE POLICY "Users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);

-- Users can update their own data
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);

-- Users can delete their own data
CREATE POLICY "Users can delete own posts"
ON posts FOR DELETE
USING (auth.uid() = user_id);

-- Public read, authenticated write
CREATE POLICY "Public can read posts"
ON posts FOR SELECT
USING (true);

CREATE POLICY "Authenticated users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.role() = 'authenticated');

-- Role-based access
CREATE POLICY "Admins have full access"
ON posts FOR ALL
USING (
  EXISTS (
    SELECT 1 FROM users
    WHERE id = auth.uid() AND role = 'admin'
  )
);
ワークフロー 4:パフォーマンス向上のためのインデックス

目的: クエリパフォーマンスを最適化します

実装:

-- Single Column Index
CREATE INDEX idx_users_email ON users(email);

-- Composite Index
CREATE INDEX idx_posts_user_created ON posts(user_id, created_at DESC);

-- Partial Index
CREATE INDEX idx_active_users ON users(email) WHERE is_active = true;

-- Full-Text Search Index
CREATE INDEX idx_posts_search ON posts
USING gin(to_tsvector('english', title || ' ' || content));

-- Query with full-text search
SELECT * FROM posts
WHERE to_tsvector('english', title || ' ' || content)
@@ plainto_tsquery('search term');
ワークフロー 5:高度なパターン

目的: 一般的なデータベースパターンを実装します

論理削除:

CREATE TABLE posts (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  title TEXT NOT NULL,
  deleted_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_posts_not_deleted ON posts(id) WHERE deleted_at IS NULL;

CREATE POLICY "Users see non-deleted posts"
ON posts FOR SELECT
USING (deleted_at IS NULL);

監査証跡:

CREATE TABLE audit_log (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  table_name TEXT NOT NULL,
  record_id UUID NOT NULL,
  action TEXT NOT NULL CHECK (action IN ('INSERT', 'UPDATE', 'DELETE')),
  old_data JSONB,
  new_data JSONB,
  user_id UUID REFERENCES users(id),
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE OR REPLACE FUNCTION audit_trigger()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO audit_log (table_name, record_id, action, old_data, new_data, user_id)
  VALUES (
    TG_TABLE_NAME,
    COALESCE(NEW.id, OLD.id),
    TG_OP,
    CASE WHEN TG_OP = 'DELETE' THEN to_jsonb(OLD) ELSE NULL END,
    CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN to_jsonb(NEW) ELSE NULL END,
    auth.uid()
  );
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER posts_audit
AFTER INSERT OR UPDATE OR DELETE ON posts
FOR EACH ROW EXECUTE FUNCTION audit_trigger();

タイムスタンプの自動更新:

CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER posts_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW EXECUTE FUNCTION update_updated_at();

クイックリファレンス

タスクSQL パターン
テーブルを作成CREATE TABLE name (columns)
列を追加ALTER TABLE name ADD COLUMN col TYPE
インデックスを追加CREATE INDEX name ON table(columns)
RLS を有効化ALTER TABLE name ENABLE ROW LEVEL SECURITY
ポリシーを作成CREATE POLICY name ON table FOR action USING (condition)
関数を作成CREATE FUNCTION name() RETURNS type AS $$ ... $$
トリガーを作成CREATE TRIGGER name AFTER/BEFORE action ON table

Supabase でのマイグレーション

# Create Migration
npx supabase migration new create_posts_table
-- supabase/migrations/20240101000000_create_posts_table.sql

CREATE TABLE posts (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  title TEXT NOT NULL,
  content TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_posts_user_id ON posts(user_id);

ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can read own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
# Apply Migration
npx supabase db push

Supabase でのクエリ

// Select all
const { data } = await supabase.from('posts').select('*')

// Select specific columns
const { data } = await supabase.from('posts').select('id, title')

// Filter
const { data } = await supabase
  .from('posts')
  .select('*')
  .eq('user_id', userId)

// Join tables
const { data } = await supabase
  .from('posts')
  .select(`
    *,
    users (
      id,
      name,
      email
    )
  `)

// Count
const { count } = await supabase
  .from('posts')
  .select('*', { count: 'exact', head: true })

// Pagination
const { data } = await supabase
  .from('posts')
  .select('*')
  .range(0, 9)

// Order
const { data } = await supabase
  .from('posts')
  .select('*')
  .order('created_at', { ascending: false })

ベストプラクティス

  • UUID を使用する: 分散システムやセキュリティに適しています
  • タイムスタンプを追加する: 常に created_at と updated_at を含めます
  • RLS を有効にする: デフォルトですべてのテーブルで有効にします
  • 外部キーにインデックスを設定する: すべての外部キーにインデックスを作成します
  • TIMESTAMPTZ を使用する: タイムゾーンに対応するため、TIMESTAMP ではなく TIMESTAMPTZ を使用します
  • TEXT を使用する: PostgreSQL では VARCHAR ではなく TEXT を使用します
  • JSONB を使用する: パフォーマンス向上のため、JSON ではなく JSONB を使用します
  • まず正規化する: パフォーマンス上の理由がある場合にのみ非正規化します
  • マイグレーションのみを使用する: 本番環境でスキーマを直接変更しないでください
  • RLS ポリシーをテストする: 異なるユーザーコンテキストで動作を確認します
  • トランザクションを使用する: 関連する操作に使用します
  • スキーマを文書化する: ERD 図を維持します

制約

-- Primary Key
id UUID PRIMARY KEY DEFAULT uuid_generate_v4()

-- Foreign Key
user_id UUID REFERENCES users(id) ON DELETE CASCADE

-- Unique
email TEXT UNIQUE NOT NULL

-- Check
price DECIMAL CHECK (price > 0)
status TEXT CHECK (status IN ('active', 'inactive'))

-- Not Null
name TEXT NOT NULL

依存関係

# Supabase CLI for migrations
npm install -D supabase

# Generate types from schema
npx supabase gen types typescript --local > types/database.ts

エラー処理

  • 外部キー違反: アプリケーションで適切に処理します
  • 一意性制約: ユーザーにわかりやすいエラーメッセージを表示します
  • チェック制約: 挿入前にアプリケーションで検証します
  • RLS エラー: セキュリティのため、403 ではなく 404 を返します
  • 接続数の上限: コネクションプーリングを使用します

パフォーマンスのヒント

  • コネクションプーリング(PgBouncer)を使用します
  • よく使われるクエリにカバリングインデックスを追加します
  • LIMIT とカーソルベースのページネーションを使用します
  • 本番環境では SELECT * を避けます
  • プリペアドステートメントを使用します
  • pg_stat_statements で遅いクエリを監視します
  • テーブルを定期的にバキュームします
  • 大規模なデータセットではテーブルパーティショニングを検討します

このスキルを使用する場面

次のような場合にこのスキルを呼び出してください:

  • 新しいデータベーステーブルを設計するとき
  • リレーションシップを設定するとき
  • RLS ポリシーを作成するとき
  • クエリパフォーマンスを最適化するとき
  • 論理削除を実装するとき
  • 監査証跡を設定するとき
  • マイグレーションを記述するとき
  • データベースの問題をデバッグするとき
  • データ型を選択するとき
  • インデックスを作成するとき

コメント