Database
Supabase の本体は本物の Postgres。テーブル設計、SQL、マイグレーション、 supabase-js のクエリ、関数 / トリガー / 拡張まで。
本物の Postgres である意味
- 標準 SQL がそのまま使える(JOIN / CTE / Window 関数 / Materialized View)
- 普通の
psqlで繋げる pg_dumpでエクスポート可能(ベンダーロックインなし)- Postgres の全拡張が一覧から有効化できる
- Prisma / Drizzle / TypeORM などの ORM もそのまま使える
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 で本番に未適用のものを順次適用。
主要なデータ型
| 型 | 用途 |
|---|---|
uuid | ID。gen_random_uuid() でデフォルト生成 |
text | 可変長文字列(varchar の上位互換) |
integer / bigint | 整数 |
numeric(10,2) | 小数(金額など) |
boolean | 真偽 |
timestamptz | UTC タイムゾーン付き時刻(推奨) |
date / time | 日付 / 時刻のみ |
jsonb | JSON。インデックスも貼れる |
text[] | 配列 |
citext | 大文字小文字無視の text(拡張) |
vector(N) | 埋め込み(pgvector 拡張) |
tsvector | 全文検索用 |
主キーの選び方
- uuid + gen_random_uuid() — 推奨。分散システム / クライアント生成可能
- bigint + identity — 軽量、URL に出ない時
- text(slug) — 人が読む URL(重複に注意)
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 演算子一覧
.eq("col", v)=.neq≠.gt / .gte / .lt / .lte.like("col", "%foo%").ilike大文字小文字無視.in("col", [...]).contains("tags", ["a"])配列 / jsonb 包含.containedBy.is("col", null).match({ a: 1, b: 2 })複数 = の AND.or("a.eq.1,b.eq.2")OR 条件.not("col", "is", null)
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。
主要拡張
- pgcrypto — UUID 生成、ハッシュ
- pgvector — ベクトル検索
- postgis — 地理空間
- http — DB 内から HTTP リクエスト
- pg_cron — 定期ジョブ
- pgmq — メッセージキュー
- pgaudit — 監査ログ
- Database → Extensions から有効化
Connection Pooling
Postgres は同時接続数が有限(数百)。Serverless / Edge 環境では各リクエストで接続を作るため、 PgBouncer / Supavisor(Supabase 内蔵)経由で繋ぐ。
- Direct connection:
postgres://...:5432— 通常のクライアント - Pooled (Transaction mode):
:6543— Serverless 推奨 - Pooled (Session mode):
:5432経由 — prepared statement が要るとき
バックアップ / 復元
- Pro 以上でDaily backupが自動
- Point-in-time recovery(追加機能)
- 自前で
pg_dumpも可能
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;
パフォーマンスのコツ
- EXPLAIN ANALYZE でクエリプラン確認
- 頻出 WHERE にはインデックス
- JOIN 多すぎる場合はマテリアライズドビューを検討
- 大量 INSERT は1 トランザクションでバルク
- Studio の Database → Performance でスロークエリ確認
「BaaS だから簡単」ではなく、本物の Postgresを扱っている自覚を持つ。 標準 SQL の知識がそのまま活きる。逆に SQL を知らないと中規模を超えたとき詰まる。