Supabase データベース設計:テーブル構造、リレーションとRLS完全ガイド
深夜3時。Supabase Dashboardの赤い警告「RLS not enabled」を見つめていました。ユーザーの記事データが漏洩する?この外部キー関係は正しい?多対多リレーションはどうやって作る?
正直、Supabaseを始めた時、多くの間違いを犯しました。テーブル作成後にRLSを有効化忘れ—誰でも全データを読める。外部キー設計が不適切—ユーザー削除しても記事が残る。多対多の結合テーブル、配列で試した—完全な災害。
数ヶ月試行錯誤して、Supabaseデータベース設計のパターンを理解しました。この記事でその経験をまとめます。
1. テーブル構造設計:PostgreSQL命名規則
1.1 命名規則:snake_caseが正解
PostgreSQLには癖があります:ダブルクォートなしで識別子を小文字に変換。ダブルクォートありで、そのまま尊重。
キャメルケース(UserProfile)を使うと、毎回ダブルクォートが必要。面倒すぎる。
PostgreSQLコミュニティの慣習:snake_case(アンダースコア区切り)、テーブル名複数、カラム名単数。
-- ✅ 推奨
CREATE TABLE users (
id UUID PRIMARY KEY,
email TEXT UNIQUE,
created_at TIMESTAMPTZ
);
1.2 カラム型選択:MySQL思考に囚われない
間違い1:VARCHARの代わりにTEXTを使う
PostgreSQLではTEXTとVARCHARの性能は同じ。違いはVARCHAR(n)が長さ制限あるだけ。長さ制限必要なければTEXTを使う。
間違い2:TIMESTAMPの代わりにTIMESTAMPTZを使う
TIMESTAMPはタイムゾーンを保存しない。サーバーが米国、ユーザーが中国—表示時刻が混乱。TIMESTAMPTZは自動的にタイムゾーン変換。
間違い3:SERIALの代わりにUUIDを使う
SERIALは自動増分整数。単一サーバーアプリでは問題ないが、分散システムで競合。UUIDはグローバル一意。
2. 3つのテーブルリレーション:1対1、1対多、多対多
2.1 1対1:UNIQUEを追加するだけ
最も一般的なシナリオ:ユーザーとプロフィール。
CREATE TABLE profiles (
id UUID PRIMARY KEY,
user_id UUID UNIQUE REFERENCES users(id) ON DELETE CASCADE,
bio TEXT
);
キーポイント:user_id UUID UNIQUE。UNIQUE制約で各ユーザーが1つのプロフィールのみ。
2.2 1対多:普通の外部キー
著者と書籍。1人の著者が多くの書籍を書ける。
CREATE TABLE books (
id UUID PRIMARY KEY,
author_id UUID REFERENCES authors(id) ON DELETE CASCADE,
title TEXT
);
Supabase JSでネストされた関連データを直接取得可能。
2.3 多対多:結合テーブルがキー
学生とコース。1人の学生が多くのコースを選択、1つのコースが多くの学生を持つ。
解決策:結合テーブルを作成。
CREATE TABLE enrollments (
student_id UUID REFERENCES students(id) ON DELETE CASCADE,
course_id UUID REFERENCES courses(id) ON DELETE CASCADE,
PRIMARY KEY (student_id, course_id)
);
3. Row Level Security:データベース自体が警備員
3.1 RLS「デフォルト拒否」哲学
Supabase初使用時、postsテーブルを作成、anon keyでフロントエンドからクエリ。結果—全データが返ってきた。驚いた。
SupabaseはデフォルトでRow Level Security(RLS)を有効化しない。RLSなしで、誰でもanon keyで全データを読み書き可能。
第一のルール:テーブル作成後、すぐRLSを有効化。
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
それだけではない。有効化したがポリシーなし—「全アクセス拒否」。少なくとも1つのポリシーを作成必須。
3.2 ポリシー構文:USINGとWITH CHECK
- USING:既存行をフィルタ(SELECT、UPDATE、DELETE)
- WITH CHECK:新行を検証(INSERT、UPDATE)
3.3 4つの一般的なポリシーパターン
パターン1:ユーザーが自分データにアクセス
CREATE POLICY "Users manage own data"
ON posts FOR ALL
TO authenticated
USING (user_id = auth.uid());
パターン2:公開+プライベートデータ混合
公開済みは全員可见、ドラフトは著者のみ。
パターン3:マルチテナント隔離
チームメンバーは自分チームデータのみアクセス。
パターン4:RBACロール制御
管理者は特別権限を持つ。
4. RLSパフォーマンス最適化
4.1 パフォーマンス杀手:サブクエリが各行で実行
RLSポリシーのサブクエリは、各行で実行。10万行でチーム関係チェック—クエリタイムアウト3分。
4.2 最適化1:インデックス追加
RLSポリシーで使用するカラムにインデックス必須。
CREATE INDEX idx_posts_user_id ON posts(user_id);
Supabase公式テスト:インデックスなし450ms、インデックスあり45ms。10倍改善。
4.3 最適化2:SECURITY DEFINER関数
サブクエリを関数に封装、1回のみ実行。
CREATE OR REPLACE FUNCTION user_teams()
RETURNS SETOF UUID
LANGUAGE SQL SECURITY DEFINER STABLE
AS $$ SELECT team_id FROM team_members WHERE user_id = auth.uid(); $$;
5. 実世界の例
5.1 ブログシステム:記事、カテゴリ、タグ
テーブル構造、RLSポリシー、インデックス設定の完全実装。
5.2 マルチテナントSaaS:チーム協力
チームデータ隔離、メンバーアクセス、管理者権限制御。
まとめ
キーポイント:
- snake_case命名
- UUID主キー、TEXT文字列、TIMESTAMPTZタイムスタンプ
- RLS必須有効化
- インデックス + SECURITY DEFINER関数最適化
FAQ
テーブル作成後すぐRLSを有効化必須?
PostgreSQLはsnake_caseを推奨する理由?
RLSポリシーでFOR ALLを使わない理由?
RLS状態を確認する方法?
3 min read · 公開日: 2026年4月4日 · 更新日: 2026年4月5日
関連記事
n8n ワークフロー構築:ノード接続から自動化シナリオ設計まで
n8n ワークフロー構築:ノード接続から自動化シナリオ設計まで
GitHub Actions 入門:YAMLワークフロー基礎とトリガー設定
GitHub Actions 入門:YAMLワークフロー基礎とトリガー設定
ファイアウォール設定:UFW、iptablesとセキュリティポリシー設計

コメント
GitHubアカウントでログインしてコメントできます