言語を切り替える
テーマを切り替える

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を有効化必須?
はい、これはセキュリティルール。SupabaseはデフォルトでRLSを有効化しない—誰でもanon keyで全データを読み書き可能。
PostgreSQLはsnake_caseを推奨する理由?
PostgreSQLはクォートなし識別子を小文字に変換。キャメルケースは毎回ダブルクォート必要—面倒すぎる。
RLSポリシーでFOR ALLを使わない理由?
FOR ALLは4つの別ポリシーより性能が劣る。別ポリシーでPostgreSQLが各操作でインデックス使用を最適化。
RLS状態を確認する方法?
Supabase Dashboardのデータベースページで各テーブルのRLS状態を確認。赤色は未有効化。

3 min read · 公開日: 2026年4月4日 · 更新日: 2026年4月5日

コメント

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

関連記事