Database Design について
Supabase と PostgreSQL を使って Next.js アプリを構築する開発者向けのデータベース設計スキルです。スキーマ設計、アクセス制御、マイグレーション、クエリ性能の実践方法を扱います。
主な機能
- 01スキーマとリレーション設計
- 02Row Level Security ポリシー
- 03インデックスとクエリ最適化
- 04Supabase マイグレーション
- 05論理削除と監査ログ
- 06Supabase クエリ例
活用シーン
- 01要件に基づくテーブル設計
- 02RLS による行アクセス制限
- 03頻出クエリの改善
- 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:要件からのスキーマ設計
目的: ビジネス要件をデータベーススキーマに変換します
手順:
- エンティティとリレーションシップを特定します
- 主キーと外部キーを定義します
- 適切なデータ型を選択します
- 制約とデフォルト値を追加します
- パフォーマンス向上のためにインデックスを作成します
実装:
-- 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 ポリシーを作成するとき
- クエリパフォーマンスを最適化するとき
- 論理削除を実装するとき
- 監査証跡を設定するとき
- マイグレーションを記述するとき
- データベースの問題をデバッグするとき
- データ型を選択するとき
- インデックスを作成するとき
コメント
Markdown 対応