Database

Supabase の本体は本物の Postgres。テーブル設計、SQL、マイグレーション、 supabase-js のクエリ、関数 / トリガー / 拡張まで。

本物の Postgres である意味

Studio(GUI)

ダッシュボード内のスプレッドシート風 UI。テーブル定義 / SQL 実行 / インデックス作成 / RLS ポリシー編集まで。 ローカル CLI 起動時は http://localhost:54323

テーブルの作り方

SQL Editor で書く

create table public.profiles (
  id uuid primary key references auth.users(id) on delete cascade,
  username text unique not null,
  display_name text,
  avatar_url text,
  bio text,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create index profiles_username_idx on public.profiles (lower(username));

Studio で GUI

Tables → New table。型・NOT NULL・デフォルト・FK が選べる。学習中は GUI、 本番は SQL ファイルで管理が定石。

マイグレーション

DDL(テーブル定義の SQL)をファイルとして履歴化する。

supabase migration new create_profiles
# supabase/migrations/20260101120000_create_profiles.sql が生成される

# SQL を書いてからローカル DB に適用
supabase db reset

# 本番に反映
supabase db push

migration ファイルの書き方

-- 20260101120000_create_profiles.sql
create table public.profiles (...);
create policy ...;

1 ファイル = 1 変更db push で本番に未適用のものを順次適用。

主要なデータ型

用途
uuidID。gen_random_uuid() でデフォルト生成
text可変長文字列(varchar の上位互換)
integer / bigint整数
numeric(10,2)小数(金額など)
boolean真偽
timestamptzUTC タイムゾーン付き時刻(推奨)
date / time日付 / 時刻のみ
jsonbJSON。インデックスも貼れる
text[]配列
citext大文字小文字無視の text(拡張)
vector(N)埋め込み(pgvector 拡張)
tsvector全文検索用

主キーの選び方

auth.users との連携

Supabase はauth スキーマauth.users テーブルを持つ。 アプリ用のプロフィール情報はpublic.profiles 等を作って FK で繋ぐ。

create table public.profiles (
  id uuid primary key references auth.users(id) on delete cascade,
  ...
);

-- ユーザ作成時に profiles も自動作成
create function public.handle_new_user()
returns trigger as $$
begin
  insert into public.profiles (id, username)
  values (new.id, new.email);
  return new;
end;
$$ language plpgsql security definer;

create trigger on_auth_user_created
  after insert on auth.users
  for each row execute function public.handle_new_user();

supabase-js の CRUD

SELECT

// 全件
await supabase.from("posts").select("*")

// 列指定
await supabase.from("posts").select("id, title, created_at")

// 条件
await supabase.from("posts")
  .select()
  .eq("status", "published")
  .gte("created_at", "2026-01-01")
  .order("created_at", { ascending: false })
  .limit(20)

// 1 件
await supabase.from("posts").select().eq("id", id).single()

// ページング
await supabase.from("posts").select().range(0, 19)  // 0〜19 行

where 演算子一覧

INSERT

// 1 件
const { data, error } = await supabase
  .from("posts")
  .insert({ title: "hello", content: "..." })
  .select()
  .single()

// 複数件
await supabase.from("posts").insert([
  { title: "a" }, { title: "b" }
])

// upsert
await supabase.from("posts")
  .upsert({ id, title: "..." }, { onConflict: "id" })

UPDATE

await supabase.from("posts")
  .update({ title: "new" })
  .eq("id", id)
  .select()

DELETE

await supabase.from("posts")
  .delete()
  .eq("id", id)

JOIN(PostgREST 風)

外部キー関係をたどる埋め込み構文:

// posts に author_id があり、profiles と FK 関係
await supabase
  .from("posts")
  .select(`
    id,
    title,
    author:profiles ( id, username, avatar_url ),
    comments ( id, body, created_at )
  `)
  .eq("id", id)
  .single()

// 結果
// {
//   id, title,
//   author: { id, username, avatar_url },
//   comments: [...]
// }

逆方向(1 対多)

// profiles に紐づく posts を取得
await supabase
  .from("profiles")
  .select(`
    id, username,
    posts ( id, title )
  `)

同名テーブルが複数 FK で繋がるとき

// FK 名で曖昧解消
await supabase
  .from("messages")
  .select(`
    id, body,
    sender:profiles!sender_id ( username ),
    receiver:profiles!receiver_id ( username )
  `)

集計

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

// 集計関数(PostgREST 12+)
await supabase
  .from("posts")
  .select("category, count:id.count(), avg_views:views.avg()")

関数(RPC)

SQL 関数を作って RPC 経由で呼ぶ。複雑な計算は SQL 側に置く方が速い。

create or replace function get_top_authors(min_posts int default 3)
returns table(author_id uuid, post_count bigint)
language sql
as $$
  select author_id, count(*)::bigint
  from posts
  group by author_id
  having count(*) >= min_posts
$$;
const { data } = await supabase.rpc("get_top_authors", { min_posts: 5 })

トリガー

-- updated_at を自動更新
create or replace function set_updated_at()
returns trigger as $$
begin
  new.updated_at = now();
  return new;
end;
$$ language plpgsql;

create trigger profiles_updated_at
  before update on profiles
  for each row execute function set_updated_at();

インデックス

-- 通常の B-tree
create index posts_created_idx on posts (created_at desc);

-- 複合
create index posts_user_status_idx on posts (user_id, status);

-- ユニーク
create unique index posts_slug_uniq on posts (slug);

-- 関数式
create index posts_lower_title on posts (lower(title));

-- 部分インデックス
create index posts_published_idx on posts (created_at)
  where status = 'published';

-- jsonb 用
create index posts_meta_gin on posts using gin (meta);

-- 全文検索
create index posts_fts on posts using gin (to_tsvector('simple', title || ' ' || content));

JSONB の扱い

-- データ
update posts set meta = '{"tags": ["js","react"], "views": 100}' where id = ...;

-- パスで参照
select meta->'tags', meta->>'views' from posts;

-- where
select * from posts where meta @> '{"tags":["js"]}';

-- supabase-js
await supabase.from("posts").select().contains("meta", { tags: ["js"] })

全文検索

-- カラムを生成
alter table posts add column fts tsvector
  generated always as (to_tsvector('simple', title || ' ' || coalesce(content, ''))) stored;

create index posts_fts_idx on posts using gin (fts);
// supabase-js
await supabase.from("posts")
  .select("*")
  .textSearch("fts", "react & performance", { type: "websearch" })

pgvector(埋め込み検索)

create extension if not exists vector;

create table embeddings (
  id uuid primary key default gen_random_uuid(),
  content text,
  embedding vector(1536)  -- OpenAI text-embedding-3-small
);

-- 近傍検索(HNSW インデックス)
create index on embeddings using hnsw (embedding vector_cosine_ops);

-- 類似クエリ
select content, 1 - (embedding <=> $1) as similarity
from embeddings
order by embedding <=> $1
limit 5;

AI / RAG の基盤がそのまま Supabase 内で完結する。詳細は AI Agent: RAG

主要拡張

Connection Pooling

Postgres は同時接続数が有限(数百)。Serverless / Edge 環境では各リクエストで接続を作るため、 PgBouncer / Supavisor(Supabase 内蔵)経由で繋ぐ。

バックアップ / 復元

pg_dump "postgres://postgres:[PASSWORD]@db.xxx.supabase.co:5432/postgres" > backup.sql

Realtime と Replica Identity

Realtime で UPDATE / DELETE のイベントを受け取るには replica identity full が必要なことがある:

alter table posts replica identity full;

パフォーマンスのコツ

本物の DB

「BaaS だから簡単」ではなく、本物の Postgresを扱っている自覚を持つ。 標準 SQL の知識がそのまま活きる。逆に SQL を知らないと中規模を超えたとき詰まる。