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

Supabase データベース設計:テーブル構造・リレーション・Row Level Security 完全ガイド

Supabase Dashboard に表示された赤い警告マーク――「RLS not enabled」をじっと見つめる。頭の中にいくつもの疑問が浮かぶ。ユーザーの記事データは漏れないだろうか? この外部キーのリレーションは正しいのか? 多対多のリレーションは結局どう作ればいいのか?

Supabase を使い始めたころは、本当にたくさんの落とし穴にはまりました。テーブルを作ったのに RLS の有効化を忘れ、誰でも全データを読み取れる状態になっていたこと。外部キーの設計がずさんで、ユーザーデータを削除しても記事が残ってしまったこと。多対多の中間テーブルを、なんと配列で代用しようとして――完全な大惨事になったこと。

数か月もがいて、ようやく Supabase のデータベース設計の勘どころがつかめてきました。この記事では、その経験をまとめて紹介します。

一、テーブル構造の設計: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:TEXT ではなく VARCHAR を使う

PostgreSQL では、TEXT と VARCHAR のパフォーマンスは完全に同じです。違いは VARCHAR(n) に長さ制限があるだけ。本当に長さを制限したい場合を除けば、そのまま TEXT を使いましょう。

間違い 2:TIMESTAMPTZ ではなく TIMESTAMP を使う

TIMESTAMP はタイムゾーン情報を保存しません。サーバーがアメリカ、ユーザーが中国にいると、表示時刻がめちゃくちゃになります。TIMESTAMPTZ なら自動でタイムゾーンを変換してくれます。

間違い 3:UUID ではなく SERIAL を使う

SERIAL は自動採番の整数です。単一マシンのアプリなら問題ありませんが、分散システムでは衝突します。UUID はグローバルに一意です。

二、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)
);

三、Row Level Security:データベース自身が用心棒になる

3.1 RLS の「デフォルト拒否」という思想

私が初めて Supabase を使ったとき、posts テーブルを作り、anon key を使ってフロントエンドから直接クエリしました。その結果――すべてのデータが返ってきたのです。ぎょっとしました。

実は Supabase はデフォルトで Row Level Security(RLS)が無効になっています。有効化していないと、anon key を手に入れた人なら誰でも全データを読み書きできてしまいます。

そこで第一の鉄則です。テーブルを作ったら、すぐに RLS を有効化する

ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

RLS を有効化したら終わりかというと、まだです。有効化してもポリシーがなければ「すべてのアクセスを拒否」と同じ意味になります。少なくとも 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 によるロール制御

管理者には特別な権限があります。

四、RLS のパフォーマンス最適化

4.1 パフォーマンスの大敵:サブクエリが行ごとに 1 回ずつ実行される

RLS ポリシー内のサブクエリは、データの 1 行ごとに 1 回実行されます。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.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 状態を確認できます。赤色は未有効を意味します。

2分で読めます · 公開日: 2026年4月4日 · 更新日: 2026年6月8日

関連記事

コメント

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