Postgresのスキーマ設計プランニングモードは、コード生成の前にエンティティ、制約、インデックス、マイグレーションを定義する手助けをし、後の書き直しを減らします。

エンドポイントやモデルをデータベースの形が定まる前に作ると、同じ機能を二度書き直すことがよくあります。デモでは動いていても、本番データやエッジケースが来ると脆く感じ始めます。
多くの書き直しは次の三つの予測できる問題から来ます:
どれもコード、テスト、クライアントアプリに波及する変更を強います。
Postgresスキーマを計画するということは、まずデータ契約を決め、それに一致するコードを生成することです。実務では、エンティティ、関係、重要なクエリを洗い出し、ツールがテーブルやCRUDを作る前に制約、インデックス、マイグレーション方針を選ぶ、という流れになります。
これはKoder.aiのようなvibe-codingプラットフォームを使う場合に特に重要です。多くのコードを素早く生成できるのは良いことですが、スキーマが固まっているときの方がはるかに信頼性が高いです。生成されたモデルやエンドポイントは後で手直しが少なくて済みます。
計画を飛ばすと典型的に起きること:
良いスキーマ計画はシンプルです:エンティティの普通の言葉での説明、テーブルとカラムの草案、主要な制約とインデックス、そして拡張時に安全に変更できるマイグレーション戦略。
スキーマ設計は、アプリが何を記憶しなければならないか、ユーザーがそのデータで何をできるべきかから始めると最も効果的です。目標を2〜3文で書いてください。簡単に説明できなければ、不要なテーブルを作ってしまう恐れがあります。
次に、データを作成・変更するアクションに注目します。これらのアクションが行の本当の発生源であり、何を検証すべきかを明らかにします。名詞ではなく動詞で考えてください。
例えば、予約アプリなら「予約を作成する、日程を変更する、キャンセルする、返金する、顧客にメッセージを送る」といった動詞が浮かびます。これらの動詞は、テーブルの名前を付ける前に保存すべきもの(時間枠、ステータス変更、金額など)を示唆します。
読み取り経路も記録してください。読み取りが構造やインデックス設計を決めます。「自分の予約」画面は日付順でステータスで絞る、管理者は顧客名や予約参照で検索できる、拠点別の日次収益、誰がいつ何を変更したかの監査ビュー、などです。
最後に、監査履歴、ソフトデリート、マルチテナント分離、プライバシールール(連絡先詳細の閲覧制限など)といった非機能要件をメモしてください。
これを書き留めてから生成するなら、そのメモが強力なプロンプトになります。何が必須で、何が変わり得て、何が検索可能であるべきかを明確にします。Koder.aiを使うなら、生成前にこれを書いておくとPlanning Modeの効果が高まります。
テーブルを触る前に、アプリが保存するものを平易に書いてください。繰り返し出てくる名詞をリストアップします:user、project、message、invoice、subscription、file、comment。各名詞がエンティティ候補です。
次に、各エンティティについて「それは何か、なぜ存在するか」を1文で書きます。例:「Projectは、ユーザーが作成して作業をグループ化し、他を招待するためのワークスペースである」。これにより data や items、misc のような曖昧なテーブルを避けられます。
所有権は重要な決定で、ほとんどのクエリに影響します。各エンティティについて:
次にレコードの識別方法を決めます。UUIDはウェブ・モバイル・バックグラウンドジョブなど多くの場所で作られる場合や予測可能なIDを避けたい場合に有効です。bigintは小さく高速です。人に親しみやすい識別子が必要なら、主キーとは別に短い project_code のような一意カラムを設けてください。
最後に、関係を図にする前に文章で書きます:ユーザーは複数のプロジェクトを持つ、プロジェクトは複数のメッセージを持つ、ユーザーは複数のプロジェクトに属することができる、など。各リンクを必須か任意かでマークします(「メッセージは必ずプロジェクトに属する」 vs 「請求書はプロジェクトに属する場合がある」)。これらの文章が後のコード生成のソースオブトゥルースになります。
平易な言葉でエンティティが明確になったら、それぞれをテーブルに変換し、必要な事実を保存するカラムを決めます。
まずは命名と型のパターンを一貫させてください:snake_case のカラム名、同じ意味には同じ型、予測可能な主キー。タイムスタンプには timestamptz を推奨します。金額は numeric(12,2)(またはセントを整数で保存)を使い、浮動小数点は避けてください。
ステータスフィールドは Postgres の enum か text 列に CHECK 制約を付けて許容値を管理します。
必須か任意かはルールを NOT NULL に翻訳して決めます。行が意味を成すために値が必須なら NOT NULL に、真に不明または該当しない場合は NULL を許容します。
実務的なデフォルトカラムのセット:
id(uuid または bigint、どちらかを選んで一貫させる)created_at と updated_atdeleted_at(ソフトデリートと復元が本当に必要な場合のみ)created_by(誰が何をしたかの明確な監査が必要な場合)多対多の関係はほとんどの場合ジョインテーブルにしてください。例えば複数のユーザーが共同作業できるなら app_members を作り、app_id と user_id を持たせ、ペアに一意制約をかけて重複を防ぎます。
バージョン管理が必要なら、最初から不変テーブル(例:app_snapshots)を計画し、各行を app_id で関連付け created_at を持たせます。
制約はスキーマのガードレールです。どのサービス、スクリプト、管理ツールが触っても成り立つべきルールを決めてください。
まずは識別と関係から。すべてのテーブルに主キーが必要で、「belongs to」フィールドは単なる整数ではなく実際の外部キーにしてください。
その後、重複が問題になる箇所に一意性を追加します(例:同じメールのアカウントが二つあることは問題)。
早めに計画しておくべき高価値な制約:
amount >= 0、status IN ('draft','paid','canceled')、rating BETWEEN 1 AND 5 のような安価なルールCascade の振る舞いは計画の効果が大きく出る部分です。実際に人々が何を期待するかを考えてください。顧客が削除されたとき注文が消えるべきではないことが多く、この場合は削除を制限して履歴を残す方が望ましいでしょう。一方、注文の明細のように親なしでは意味がないデータには CASCADE が適していることがあります。
後でモデルとエンドポイントを生成するとき、これらの制約は必須要件になります:どんなエラーを扱うか、どのフィールドが必須か、どのエッジケースが設計上あり得ないかが明確になります。
インデックスは一つの質問に答えます:実際のユーザーにとって何を速くする必要があるか。
最初に出荷する画面やAPIコールを基準にしてください。フィルタで絞って最新順に並べる一覧ページと、関連レコードを読み込む詳細ページでは必要なインデックスが異なります。
インデックスを選ぶ前に、5〜10件のクエリパターンをプレーンな英語(または平易な言葉)で書き出してください。例:「過去30日間の請求書を表示、支払い済み/未払いでフィルタ、created_atでソート」や「プロジェクトを開いてタスクを due_date で一覧する」など。これがインデックス選びを現実に根ざしたものにします。
よく使われる最初のインデックスセットは、ジョインに使う外部キー、共通のフィルタカラム(status、user_id、created_at)と、安定した複合フィルタ用に1〜2個の複合インデックスです。例:常に account_id でフィルタして時間でソートするなら (account_id, created_at) のような複合インデックスが有効です。
複合インデックスの順序は重要です。最も選択性が高く、最も頻繁にフィルタされるカラムを先頭に置いてください。例えばテナントIDを毎回フィルタするなら、多くのインデックスで先頭に置くべきです。
「念のために全部インデックスを貼る」は避けてください。各インデックスはINSERTやUPDATE時に追加のコストを生み、稀なクエリを少し速くするために全体の性能を下げることがあります。
全文検索は別に計画してください。単純な「含む」検索で十分なら最初は ILIKE で良いこともありますが、検索が重要なら早めに全文検索(tsvector)を計画しておくと後で設計をやり直さずに済みます。
スキーマは最初のテーブル作成で「完成」するものではありません。機能追加や誤り修正、データからの学びで変わり続けます。最初にマイグレーション戦略を決めておけば、コード生成後の辛い書き直しを避けられます。
簡単なルール:データベース変更は小さく、機能単位で行ってください。各マイグレーションはレビューしやすく、どの環境でも安全に実行できるべきです。
多くの破壊的変更はカラムの名前変更・削除や型変更から生じます。一度に全部やるのではなく、安全なステップを計画してください:
ステップは増えますが、ダウンタイムや緊急パッチを減らせるので現実的には速く安全です。
シードデータもマイグレーションの一部です。役割、ステータス、国、プラン種別など「常に存在する」参照テーブルを決めて、それらの挿入・更新は専用のマイグレーションに入れておくと、全開発者・全デプロイで一致した状態になります。
早めに期待値を設定してください:
ロールバックが常に完璧な「ダウン」移行であるとは限りません。場合によってはバックアップ復元が最良のロールバックです。Koder.aiを使っている場合は、大きな変更の前にスナップショットを使って素早く戻せる方針を決めておくと便利です。
小さなSaaSアプリを想像してください。人々がチームに参加し、プロジェクトを作り、タスクを追跡します。
まずエンティティと、初日に必要な最低限のフィールドを並べます:
関係は明快です:チームは複数のプロジェクトを持ち、プロジェクトは複数のタスクを持ち、ユーザーは team_members を通じてチームに参加します。タスクはプロジェクトに属し、ユーザーに割り当てられる場合があります。
次に、典型的なバグを防ぐいくつかの制約を追加します:
インデックスは実際の画面に合わせます。例えばタスクリストが project と state でフィルタして最新順に並べるなら、tasks (project_id, state, created_at DESC) のようなインデックスを計画します。「自分のタスク」が重要なビューなら tasks (assignee_user_id, state, due_date) のようなインデックスが役立ちます。
マイグレーションは最初は安全で地味に:テーブル作成、主キー、外部キー、コアの一意制約。使用が証明された後に追加するもの(soft delete の導入やインデックスの調整など)は次の変更に回しましょう。
多くの書き直しは最初のスキーマにルールや実際の使用詳細が欠けていることが原因です。良い計画は完璧な図を作ることではなく、早く罠を見つけることです。
一般的な誤りは重要なルールをアプリケーションコードだけに置いてしまうことです。一意であるべき、必須であるべき、範囲内であるべき値はデータベース側で強制すべきです。さもないとバックグラウンドジョブや新しいエンドポイント、手動インポートがロジックを迂回してしまいます。
インデックスを後回しにするのもよくある見落としです。ローンチ後に追加すると推測ベースになりがちで、本当の遅いクエリがジョインやステータスであるのに別のものをインデックスしてしまうことがあります。
ジョインテーブルも静かなバグの原因です。重複を防ぐ制約がないと同じ関係が二重に保存され、原因究明に時間を使うことになります。
監査ログ、ソフトデリート、イベント履歴が後から必要になることも普通です。それらを後から足すとエンドポイントやレポートに波及します。
最後に、JSONカラムは柔軟性の誘惑がありますが、チェックが外れ、インデックスしにくくなる点に注意してください。コアのビジネスフィールドには向かず、本当に可変なペイロードだけに使うべきです。
生成前のクイック修正リスト:
user_id + role_id)ここで一旦止まり、計画がモデル生成を驚きなしで進められるほど完成しているか確認してください。目標は完璧ではなく、後で書き直しを招く欠落を見つけることです:関係が抜けている、ルールが不明確、インデックスが実際の使い方に合っていない、など。
短いプレフライトチェック:
amount >= 0 や許可ステータス)簡易テスト:明日チームメンバーが来て最初のエンドポイントを作ると想定してください。その人が毎時間「これはNULLにして良い?」「削除されたら何が起きる?」と聞かずに済む計画になっていますか?
計画が読みやすく、主要なフローが紙の上で意味を持つようになったら、それを実行可能なものに変えます:実際のスキーマとマイグレーション。
まず初期マイグレーションでテーブル、型(enumを使うなら)と必須の制約を作ります。最初は小さくても正しいパスを取り、少しのシードデータを入れてアプリで必要なクエリを実行してみてください。フローが違和感あるなら、マイグレーション履歴が短いうちにスキーマを直してください。
生成は、スキーマが安定していて翌日全面的に名前を変える必要がない時に最も速くなります。初期の基本操作(作成、更新、一覧、削除、+1つの実際のビジネスアクション)をスキーマ上でテストしてからモデルとエンドポイントを生成してください。
書き直しを少なく保つ実務ループ:
何をDBで検証し、何をAPI層で検証するか早めに決めてください。永続ルール(外部キー、一意制約、チェック制約)はデータベースに入れ、柔らかいルール(フラグ、テンポラリな制限、頻繁に変わる複雑なクロステーブルロジック)はAPI側に置くのが基本です。
Koder.aiを使う場合は、まずPlanning Modeでエンティティとマイグレーションを合意し、その後Go+PostgreSQLバックエンドを生成するのが賢明です。もし変更で問題が起きたら、スナップショットとロールバックで既知の良いバージョンに戻し、スキーマ計画を調整できます。
スキーマを先に計画してください。それが安定したデータ契約(テーブル、キー、制約)を作り、生成されたモデルやエンドポイントがあとで何度もリネームや書き直しを必要としなくなります。
実務では:エンティティ、関係、主要なクエリを書き出し、コードを生成する前に制約、インデックス、マイグレーションを固めてください。
アプリが何を記録し、ユーザーがそのデータで何ができるべきかを2〜3文で書いてください。
次に:
これだけで表を過剰に作らずに設計を始められます。コレクションや図にこだわりすぎないのが早く進めるコツです。
繰り返し出てくる名詞(user、project、invoice、task など)をリストアップして始めてください。各々について「それは何か、なぜ存在するか」を1文で書きます。
説明できないものは曖昧なテーブル(items や misc)になりがちです。明確さがコアエンティティを決める助けになります。
スキーマ全体で一貫したID戦略をデフォルトにしてください。
人間向けの識別子が必要なら、主キーとは別に一意のカラム(例:project_code)を追加してください。
関係ごとに期待される挙動に基づいて決めてください。
一般的なデフォルト:
RESTRICT/NO ACTION を使うCASCADE を使っても良い(例:注文 → 注文明細)これは早い段階で決めると、APIの挙動やエッジケースに与える影響が小さくなります。
データベースには永続的なルールを入れて、どのライター(API、スクリプト、インポート、管理ツール)も従うようにしてください。
優先順位:
推測ではなく実際のクエリパターンから始めてください。
5〜10件のプレーンなクエリ(フィルタ+ソート)を書き出し、それに合わせてインデックスを決めます:
status、user_id、created_at のような共通フィルタ(account_id, created_at))すべてにインデックスを貼ると挿入や更新が遅くなるので避けてください。
ジョインテーブルを作り、2つの外部キーと複合一意制約を追加してください。
例:
team_members(team_id, user_id, role, joined_at)UNIQUE (team_id, user_id) を追加して重複を防ぐこれで「なぜこのユーザーが二回見えるのか?」のような微妙なバグを避けられます。
デフォルト:
timestamptz(タイムゾーンの驚きを減らす)numeric(12,2) やセントを整数で保存(浮動小数点は避ける)CHECK で強制する同じ概念には同じ型を使うことで、結合やバリデーションが予測可能になります。
小さくてレビューしやすいマイグレーションを使い、一度に壊れる変更を避けます。
安全な手順:
シード/参照データもマイグレーションに含めて、全環境で同じ結果になるようにしてください。
PRIMARY KEYFOREIGN KEYUNIQUE(例:メール、ジョインテーブルの (team_id, user_id))CHECK(非負の金額、許可されたステータスなど)NOT NULL