「これ、前の案件でも書いた気がする」
先週、別のクライアント向けに Supabase の RLS ポリシーを書いていて、ふと手が止まった。半年前、まったく別の会社でほぼ同じロジックを書いた記憶がある。ローカルの雑多なフォルダを漁ると、案の定、8割同じ SQL が出てきた。
1人でスタジオを回していると、案件をまたぐたびに「作ったモノ」がどこかに散らばる。Notion にも、Git のプライベートリポジトリにも、Raycast のスニペットにも、ChatGPT のログにさえ、断片は残っている。ただ「今、どれが再利用可能で、どれが本当に新規なのか」を数字で言うことはできなかった。
「毎回ゼロから作っている気がする」を定量化できないのは、それなりに困る。工数見積もりは体感で滑り、価格の根拠が曖昧になり、次に何を投資して整えるべきかも決まらない。
これから3回に分けて、大げさなデータ基盤なしに、受託の資産再利用を SQL 二本で測るための最小設計を書いていく。1回目は、テーブルを引く前の「設計思想」の話に絞る。実装のディテールと再利用率クエリは2回目、複数人で書く場合の運用は3回目に譲る。

なぜ「オントロジー」を作らないのか
再利用を測ろう、と考えた最初、頭に浮かぶのは分類体系(オントロジー)だ。「プロンプト系」「SQL 系」「フロントコンポーネント系」…と型を切り、階層を作り、後から検索できるようにする。設計としては綺麗に見える。
やらないほうがいい、と決めた。
理由は単純で、1人スタジオの案件は分野が飛ぶ。今週はデータパイプライン、来週はランディングページ、その次は社内 Slack ボット、みたいなことが普通に起きる。半年後には確実に「今の分類に収まらないモノ」が出てくる。そのたびに階層を組み替えたり、正規化を悩んだりする時間は、そもそも本業を圧迫する。
大手製造業の設計資産管理は、結局 PLM(Product Lifecycle Management)なしでは難しい、という論調をよく見る※1。それはその通りだと思う。ただ、こちらは机ひとつと GitHub のリポジトリいくつかの世界だ。PLM の代わりに、テーブルを2つだけ持つ。
大事なのは、「オントロジーを作らない」ためのオントロジーを作ることだ。分類を放棄するのではなく、分類より先に「算数」を成立させる。
算数のための最小構造
やりたいことをクエリ側から書き下してみる。
直近6ヶ月で、新規作成した資産の割合はいくつか
案件 A は、既存資産をどれだけ再利用したか
どの資産が、いくつの案件で使われているか
これだけなら、必要な構造はほとんど決まってしまう。
create extension if not exists vector;
-- 作ったモノ本体。分類体系は持たない
create table assets (
id uuid primary key default gen_random_uuid(),
kind text not null, -- 'prompt' | 'sql' | 'component' | 'workflow' など
-- 型は自由に増える。増えて構わない
name text not null,
body text, -- 散文。何のためのモノか、どういう文脈で作ったか
-- 埋め込みの元にもなる
embedding vector(1536), -- 検索用。実装は後続回
created_at timestamptz default now()
);
-- 資産と案件の関係。ここだけが「算数」を担う
create table project_assets (
id uuid primary key default gen_random_uuid(),
project text not null, -- 誰の、いつの案件か。ただの文字列でよい
asset_id uuid references assets(id) not null,
relation text not null, -- 'created' | 'reused' の2択で始める
instances int default 1, -- 同じ案件内で何回使ったか
created_at timestamptz default now()
);これで終わり。テーブルは2つ。ビジネスロジックはほぼゼロ。
Supabase の新規プロジェクトを立てて、SQL エディタにこの15行ほどを貼れば、明日から数え始められる。
設計判断の「なぜ」
このスキーマ、盛るところは無限にある。あえて盛らなかった箇所と、その理由を並べておく。
なぜ kind に enum や外部テーブルを使わないのか
kind text で書くと、'prompt' と 'Prompt' が混ざる、みたいな事故が起きる。それは十分に承知したうえで、ENUM も分類マスタも作らない。
理由は先に書いた通り、案件の分野が飛ぶからだ。半年後に「これは prompt でも sql でもない、workflow に近い何か」が現れたとき、マイグレーションを走らせて enum を追加し、他のコードにも波及させる、という工程はまず放棄される。放棄されたルールは、存在しないより悪い。ルールがあるのに守られていない状態は、判断を鈍らせるからだ。
代わりに、四半期に一度、select distinct kind from assets を眺めて、必要なら手で寄せる。テキストのゆらぎは、あとから rename すればいい。集計の初期段階でこの手間を先送りできるメリットのほうが大きい。
なぜ relation は2択だけか
'created' | 'reused'。それだけだ。「参考にした」「一部流用」「概念だけ引き継いだ」──思いつく限りの中間状態が存在する。全部捨てた。
主観の入り込む余地を残すと、記録すること自体が面倒になる。1人スタジオの記録は「面倒になった瞬間に死ぬ」。8割の再利用が 'reused' に丸められるのは荒っぽいが、荒っぽくても記録が続くほうが、精密で消えるより強い。
なぜ project は文字列で、外部キーを張らないのか
「プロジェクト」の定義は、現場ごとに驚くほど違う。1つの契約が3プロジェクトに分かれる場合もあれば、1年間ぼんやり続く保守が全部1プロジェクト扱いの場合もある。
先に projects テーブルを切ると、その定義を確定させる仕事が生まれる。そこに時間を溶かしたくない。GROUP BY できれば集計は成立する。あとで本当に必要になったら、project_assets.project の distinct を眺めながら、そのときに切ればいい。
なぜ Supabase 一つに全部乗せるのか
再利用「率」を測るには関係型が要る。「あのとき書いたやつ、なんて名前だっけ」を思い出すには全文検索と、できれば意味検索が要る。散文の body も残しておきたい。
小さいうちは、疎結合が贅沢すぎる。ベクタ DB を別に立てて、埋め込みをキューで同期して、集計は BI ツールで、というのは、たしかに教科書的には正しい。ただ 1人スタジオの土台としては、DevOps だけで週末が終わる。
Supabase(というより PostgreSQL + pgvector)一つに、
関係型(集計):
project_assetsの SQL 集計散文(文脈):
assets.bodyの全文検索や LLM への文脈渡しベクトル(検索):
assets.embeddingの類似検索
を全部乗せる。pgvector は HNSW 索引が実質デフォルトの選択肢になり、数百万ベクトル規模まで実用的な応答が出るようになった※2。少なくとも受託案件の資産管理という用途では、これで足りなくなる日は当分こない。
使い始めの記録粒度
このスキーマ、書き終えてから最初の1週間は、記録の粒度で迷った。関数1本ずつ登録するのか。スニペット単位か。プロジェクト全体で1レコードか。
結論としては、「あとから検索して思い出したい単位」で切る、に落ち着いた。関数1本ずつは細かすぎて記録が続かない。プロジェクト単位では粒度が粗くて再利用の実態が見えない。中間の、「これ一式まとめて別案件に持って行けるな」と自分が感じる塊。それ以上は考えない。
粒度と、通貨の混在(円とウォンが同じ project 文字列上に並ぶ問題)は、次回まとめて扱う。
次回
次回は project_assets の実装と、再利用率を出す SQL を書く。
createdとreusedの比率をどう出すか案件を跨いで「一番効いている資産」をどう浮かび上がらせるか
集計の分母をどうするか(件数か、instances 合計か)
ここまでのテーブル定義があれば、あとは SELECT 文を書くだけの話ではある。ただ、その SELECT の書き方で見える景色がだいぶ変わる、という話をする予定だ。
※1: 大手 EDA/PLM ベンダの資料では、設計資産の活用は分類体系と PLM 基盤の整備が前提とされている論調が主流。
※2: Supabase の pgvector サポート状況について、HNSW 索引が新規テーブルで既定的に採用され、Pro プラン上で数百万ベクトル規模の p99 が一桁ミリ秒に収まる旨が公式ブログで報告されている。