AIエージェント時代のPostgresベストプラクティス!Supabaseが公開した「Agent Skills」を活用しよう
目次
本記事は、Supabase の公式ブログおよび supabase/agent-skills の公開ドキュメントを参考に、要点を日本語で再構成した解説記事です。コマンド例・SQL例は理解しやすいように再編したサンプルを含みます。
AIコーディングエージェントがコードを書いてくれる時代になり、CursorやClaude Code、GitHub Copilotなど、AIがデータベースのクエリやスキーマ設計まで手伝ってくれるのは本当にありがたいことです。
ただ、一つ大きな課題があります。AIエージェントはコードを書くこと自体は得意なのですが、対象のシステムを深く理解した上で「正しい」コードを書くのとは限らないのです。特にPostgresのようなデータベースでは、動くけれどパフォーマンスが悪いクエリ、セキュリティに穴のある設計が生まれがちと聞いたことがあります。
そこで Supabase が公開したのが「Postgres Best Practices for AI Agents」という取り組みです。この記事では、その内容を紹介しながら、Postgresをより安全に、より高速に使うための具体的な方法を見ていきましょう。
Supabase の「Agent Skills」とは
Supabase が公開した Agent Skills は、AIコーディングエージェントにPostgresのベストプラクティスを教えるためのナレッジベースです。
仕組みとしては、AIエージェントが参照できる形式で最適化されたドキュメント群を提供しています。31のリファレンスファイルが8つのカテゴリに分類されていて、エージェントがクエリを書いたりスキーマを設計したりする際に、これらのルールを参照して正しいコードを生成できるようになっています(リファレンス数は将来の更新で増減する可能性があります)。
インストールも簡単で、以下のコマンドひとつで導入できます。
npx skills add https://github.com/supabase/agent-skills --skill supabase-postgres-best-practices
では、公開されているガイドラインの中から、実務インパクトの大きい項目を優先度順に見ていきましょう。
8つのカテゴリと優先度
Agent Skills では、ルールが以下の8つのカテゴリに分類されています。影響度の大きいものから順に並んでいます。
| 優先度 | カテゴリ | 影響度 |
|---|---|---|
| 1 | クエリパフォーマンス | CRITICAL |
| 2 | コネクション管理 | CRITICAL |
| 3 | セキュリティ・RLS | CRITICAL |
| 4 | スキーマ設計 | HIGH |
| 5 | 並行処理・ロック | MEDIUM-HIGH |
| 6 | データアクセスパターン | MEDIUM |
| 7 | モニタリング・診断 | LOW-MEDIUM |
| 8 | 高度な機能 | LOW |
上位3つが「CRITICAL」に分類されていて、これらを放置すると大きな性能劣化やセキュリティリスクにつながる可能性があります。それぞれの具体的な内容を掘り下げていきましょう。
CRITICAL:クエリパフォーマンスの最適化
インデックスの欠落は最大の敵
※ 本記事内のSQLは、元資料の考え方を説明するために簡略化・再構成したサンプルです。
Postgresで最も多いパフォーマンス問題は、インデックスの欠落です。WHERE句やJOINで使われるカラムにインデックスがないと、テーブル全体を走査する「フルテーブルスキャン」が発生します。データ量が増えるほど影響は深刻で、100〜1000倍の速度低下につながることもあります。
まずは、典型的なアンチパターンを簡略化した例で確認してみましょう。
-- アンチパターン: インデックスなしでフィルタリング
select * from orders where customer_id = 123;
-- → テーブル全体をスキャンしてしまう
対策はシンプルで、フィルタ条件に使うカラムへインデックスを張ることです。
-- ベストプラクティス: WHERE句で使うカラムにインデックスを作成
create index orders_customer_id_idx on orders (customer_id);
JOINの場合も同様に、外部キー側にインデックスを作成します。
-- JOINで使うカラムにもインデックスを作成
create index orders_customer_id_idx on orders (customer_id);
select c.name, o.total
from customers c
join orders o on o.customer_id = c.id;
部分インデックスで効率アップ
テーブルの全行ではなく、特定の条件に合致する行だけを対象にしたインデックスを「部分インデックス(Partial Index)」と呼びます。インデックスサイズが5〜20分の1になり、書き込みもクエリも高速化できます。
-- アンチパターン: 全行を対象にしたインデックス
create index users_email_idx on users (email);
-- ベストプラクティス: アクティブなユーザーだけを対象にした部分インデックス
create index users_email_active_idx on users (email)
where deleted_at is null;
論理削除(soft delete)されたレコードや、特定のステータスのデータが大量にあるテーブルでは、部分インデックスが特に有効です。クエリ側のWHERE句とインデックスの条件を一致させることを忘れないようにしましょう。
CRITICAL:コネクション管理
Postgresの接続はコストが高い
Postgresの接続は1つあたり1〜3MBのメモリを消費します。500人の同時ユーザーがいたら、それだけで500接続が必要になり、データベースがダウンしかねません。
この問題に対して一般的なのがコネクションプーリングです。アプリケーションとデータベースの間にプーラー(PgBouncerなど)を置き、少数の実接続を多数のクライアントで共有します。
プールサイズの計算式
最適なプールサイズは以下の式で算出できます。
プールサイズ = (CPUコア数 × 2) + ディスクスピンドル数
たとえば4コアのシステムなら、プールサイズは約10接続で、500人以上の同時ユーザーをさばけます。
トランザクションモードとセッションモード
コネクションプーラーには2つのモードがあります。
- Transaction Mode(トランザクションモード): トランザクション完了後に接続をプールに返却します。サーバーレス関数やEdge Functionsのような一時的なクライアントに最適です。
- Session Mode(セッションモード): セッション全体を通して接続を保持します。Prepared Statementsやテンポラリテーブルを使う場合に必要です。
Supabase を使っている場合、PostgREST APIを多用するプロジェクトではプールサイズを全体の40%程度に抑え、それ以外の場合は80%まで割り当てるのが目安とされています。
CRITICAL:セキュリティとRow Level Security(RLS)
アプリケーション層だけのフィルタリングは危険
マルチテナント環境で「アプリ側の条件分岐だけで制御する」設計は危険です。バグや回避経路が1つあるだけで、他テナントのデータに触れてしまう可能性があります。
※ ここでいう「マルチテナント」は、1つのアプリを複数の顧客(会社・チーム)で共有する構成を指します。また「他テナント」は、自分の顧客以外のデータのことです。
Row Level Security(RLS) は、データベースレベルでアクセス制御を強制する仕組みです。行単位でアクセス権限を設定できるため、アプリケーション側にバグがあってもデータが守られます。
-- RLSを有効化
alter table orders enable row level security;
-- テーブルオーナーにもRLSを強制
alter table orders force row level security;
-- ユーザーが自分のデータだけにアクセスできるポリシーを作成
create policy "Users can only see their own orders"
on orders for select
using (user_id = (select auth.uid()));
RLSのパフォーマンスを最適化する
RLSは強力ですが、書き方を間違えるとパフォーマンスに大きな影響を与えます。特に注意したいのが、認証関数の呼び出しコストです。
-- アンチパターン: auth.uid() が行ごとに呼び出される
create policy "bad_policy" on orders
for select using (auth.uid() = user_id);
-- ベストプラクティス: select で囲んでキャッシュさせる
create policy "good_policy" on orders
for select using ((select auth.uid()) = user_id);
この小さな違いが、大きなテーブルでは100倍以上の速度差を生むことがあります。auth.uid() を (select auth.uid()) で囲むだけで、関数が1回だけ実行されてキャッシュされるようになるとのことです。
さらに、RLSポリシーで参照されるカラム(user_id など)にはインデックスを追加しておくことも重要です。
-- RLSで使うカラムにインデックスを追加
create index orders_user_id_idx on orders (user_id);
Security Definer関数でRLSの再帰を防ぐ
複雑な認可チェック(チームメンバーシップの確認など)では、security definer 関数に判定ロジックを切り出すと実装が安定します。
RLSポリシー内で参照するテーブルにもRLSがかかっていると、ポリシー評価中にさらに別のポリシー評価が連鎖し、クエリが遅くなったり意図しない挙動になったりします。
security definer 関数に判定を集約しておけば、その再帰的な評価を避けつつ、ポリシー本体をシンプルに保てます。
-- チームメンバーシップを確認する関数
create or replace function is_team_member(team_id bigint)
returns boolean
language sql
security definer
set search_path = ''
as $$
select exists (
select 1 from public.team_members
where team_members.team_id = $1
and team_members.user_id = (select auth.uid())
);
$$;
ただし注意点もあります。security definer 関数は「作成者の権限」で実行されるため、通常のRLSチェックを一部飛び越えて動きます。
そのため、APIから直接触れられる公開スキーマ(Exposed schemas)に置くと、意図せず強い権限を外部に公開してしまうリスクがあります。
この種の関数は、外部公開しないスキーマに配置し、必要な処理だけを安全に呼び出せる形にしておくのが基本です。
HIGH:スキーマ設計のベストプラクティス
適切なデータ型を選ぶ
データ型の選択はストレージ効率とクエリ性能に直結します。よくある間違いと推奨される型をまとめました。
| 用途 | アンチパターン | 推奨 | 理由 |
|---|---|---|---|
| ID | int | bigint | intは21億行でオーバーフロー |
| テキスト | varchar(255) | text | Postgresでは性能差なし |
| 日時 | timestamp | timestamptz | タイムゾーン情報を保持 |
| 真偽値 | text('true'/'false') | boolean | 1バイト vs 可変長 |
| 金額 | text | numeric(10,2) | 正確な計算が可能 |
特にIDカラムに int を使ってしまうと、約21億レコードでオーバーフローします。bigint を使えば約900京件まで対応できるので、将来的な拡張にも安心です。
MEDIUM-HIGH:並行処理とロック
トランザクションは短く保つ
長時間実行されるトランザクションはロックを保持し続け、他のクエリをブロックしてしまいます。特にやってしまいがちなのが、トランザクション内で外部APIを呼び出すパターンです。
-- アンチパターン: トランザクション内でAPIを呼び出す
begin;
select * from orders where id = 1 for update; -- ロック取得
-- ここで外部API呼び出し(2〜5秒かかる)
update orders set status = 'paid' where id = 1;
commit; -- この間ずっとロックを保持してしまう
-- ベストプラクティス: API呼び出しはトランザクションの外で行う
-- 1. まず外部APIを呼び出す(トランザクション外)
-- 2. 結果を受け取ってからトランザクションを開始
begin;
update orders set status = 'paid' where id = 1;
commit; -- ロック保持はミリ秒単位
このように非データベース処理をトランザクションの外に出すだけで、3〜5倍のスループット改善が見込めます。
安全策として statement_timeout を設定しておくのもおすすめです。
-- グローバル設定
set statement_timeout = '30s';
-- セッション単位の設定
set local statement_timeout = '5s';
MEDIUM:データアクセスパターン
N+1問題を解消する
N+1問題は、1回のクエリで取得した結果に対して、ループでN回の追加クエリを実行してしまうパターンです。データベースへの往復回数が10〜100倍になります。
-- アンチパターン: ループ内で1件ずつクエリ
-- 1回目: select * from users limit 100;
-- 2〜101回目: select * from orders where user_id = ?; (100回繰り返し)
-- → 合計101回のクエリ
-- ベストプラクティス: バッチで取得
select * from orders where user_id = any($1::bigint[]);
-- → 配列で一括指定して1回のクエリで完了
-- または JOINを使う
select u.id, u.name, o.*
from users u
left join orders o on o.user_id = u.id;
アプリケーション側でIDを集めてから any() で一括取得するか、JOINで一発取得するかのどちらかを選びましょう。
モニタリング:EXPLAIN ANALYZEで問題を特定する
パフォーマンスの問題が起きたとき、推測ではなく実際のデータに基づいて判断することが大切です。EXPLAIN ANALYZE を使えば、クエリの実行計画と実際の処理時間を確認できます。
explain (analyze, buffers, format text)
select * from orders where customer_id = 123;
出力結果で注目するポイントは以下の通りです。
- Seq Scan(シーケンシャルスキャン): インデックスが不足している可能性
- Rows Removed by Filter: フィルタで除外された行数が多い場合、インデックスの追加を検討
- Buffer reads >> cache hits: ディスクI/Oが多発しており、メモリが不足している可能性
- Nested Loop の過剰な繰り返し: JOINの方式を見直す必要あり
AIエージェントと組み合わせて使う方法
ここまでの内容を実際の開発に落とし込むには、どう使うと効果的でしょうか。実践しやすい方法をいくつか紹介します。
1. Agent Skillsをプロジェクトに導入する
先ほど紹介したコマンドでスキルをインストールすると、プロジェクトのディレクトリにリファレンスファイルが配置されます。
npx skills add https://github.com/supabase/agent-skills --skill supabase-postgres-best-practices
AIエージェントはこれらのファイルを読み取り、クエリの生成やレビュー時にベストプラクティスを参照できるようになります。
2. Claude Codeではプラグインとしてもインストールできる
Claude Codeを使っている場合は、プラグインとして直接インストールする方法も用意されています。
/plugin marketplace add supabase/agent-skills
/plugin install postgres-best-practices@supabase-agent-skills
プラグインとしてインストールすると、Claude Codeがデータベース関連のタスクを検出したときに自動的にベストプラクティスを参照してくれます。「このPostgresクエリを最適化して」や「テーブルに適切なインデックスを追加して」と指示するだけで、Agent Skillsのルールに基づいた提案が得られるようになります。
もちろん、前述の npx skills add コマンドも使えます。プロジェクトのディレクトリにファイルが配置されれば、チームメンバー全員でベストプラクティスを共有したい場合に便利です。
3. コードレビュー時にチェックポイントとして活用する
AIエージェントにコードレビューを依頼する際、以下の観点を含めるように指示するとよいでしょう。
- WHERE句やJOIN条件で使うカラムにインデックスはあるか
- RLSポリシーで
auth.uid()を(select auth.uid())で囲んでいるか - トランザクション内に外部API呼び出しが含まれていないか
- N+1パターンになっていないか
- データ型は適切か(特にIDカラムが
bigintになっているか)
とはいえ、毎回ここまで細かく指示を書く必要はありません。supabase-postgres-best-practices スキルが導入されていれば、「この変更をPostgresベストプラクティスの観点でレビューして」のように依頼するだけで、多くの場合はスキル内容に沿ってレビューしてくれます。
一方で、レビュー対象が広かったり、他のルールセットも併用していたりする場合は、「supabase-postgres-best-practices スキルを使ってレビューして」と明示したほうが意図を伝えやすく、結果も安定します。
上のチェックリストは、レビューの網羅性をさらに高めたいときの追加プロンプトとして使うイメージです。
4. 優先度に基づいて段階的に改善する
理想は、影響範囲を確認した上で全カテゴリをまとめて見直すことです。
ただし現実には工数やリスクの制約があるため、難しい場合はCRITICALなカテゴリ(クエリパフォーマンス、コネクション管理、セキュリティ)から優先的に見直していくのが効果的です。
まとめ
Supabase が公開した「Postgres Best Practices for AI Agents」の内容を紹介しました。ポイントをおさらいしましょう。
- AIエージェントはベストプラクティスを知識として持っていても、常にそれを適用してくれるとは限らないことが課題
- Agent Skillsを導入することで、エージェントにPostgresの正しい書き方を教えられる
- 優先度が最も高い3つのカテゴリはクエリパフォーマンス、コネクション管理、セキュリティ・RLS
- インデックスの追加、コネクションプーリングの導入、RLSの適切な実装が、最もインパクトの大きい改善策
(select auth.uid())のような小さな工夫で100倍以上の速度改善が見込めるケースもある
AIエージェントと一緒にデータベースを扱う機会はこれからますます増えていくはずです。こうしたベストプラクティスをプロジェクトに組み込んでおけば、AIが生成するコードの品質を底上げできます。ぜひ活用してみてください。