1 分

Postgresスキーマ設計のプランニングモード:ステップバイステップのアプローチ

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

Postgresスキーマ設計のプランニングモード:ステップバイステップのアプローチ

Postgresスキーマをコード生成の前に計画すべき理由

エンドポイントやモデルをデータベースの形が定まる前に作ると、同じ機能を二度書き直すことがよくあります。デモでは動いていても、本番データやエッジケースが来ると脆く感じ始めます。

多くの書き直しは次の三つの予測できる問題から来ます:

  • エンティティが不明確(名前やグループ化が悪い)
  • ルールが守られていない(制約が欠けている)
  • パフォーマンスの問題が遅れて表面化する(インデックスが後から追加される)

どれもコード、テスト、クライアントアプリに波及する変更を強います。

Postgresスキーマを計画するということは、まずデータ契約を決め、それに一致するコードを生成することです。実務では、エンティティ、関係、重要なクエリを洗い出し、ツールがテーブルやCRUDを作る前に制約、インデックス、マイグレーション方針を選ぶ、という流れになります。

これはKoder.aiのようなvibe-codingプラットフォームを使う場合に特に重要です。多くのコードを素早く生成できるのは良いことですが、スキーマが固まっているときの方がはるかに信頼性が高いです。生成されたモデルやエンドポイントは後で手直しが少なくて済みます。

計画を飛ばすと典型的に起きること:

  • 「User」テーブルが静かに users、admins、teams の混在になってしまう
  • 一意性が保証されず重複レコードが出る
  • 削除によって履歴が壊れる(参照挙動が未決定)
  • 人気の一覧ページが遅くなる(適切なインデックスが計画されていない)

良いスキーマ計画はシンプルです:エンティティの普通の言葉での説明、テーブルとカラムの草案、主要な制約とインデックス、そして拡張時に安全に変更できるマイグレーション戦略。

テーブルではなくデータの必要性から始める

スキーマ設計は、アプリが何を記憶しなければならないか、ユーザーがそのデータで何をできるべきかから始めると最も効果的です。目標を2〜3文で書いてください。簡単に説明できなければ、不要なテーブルを作ってしまう恐れがあります。

次に、データを作成・変更するアクションに注目します。これらのアクションが行の本当の発生源であり、何を検証すべきかを明らかにします。名詞ではなく動詞で考えてください。

例えば、予約アプリなら「予約を作成する、日程を変更する、キャンセルする、返金する、顧客にメッセージを送る」といった動詞が浮かびます。これらの動詞は、テーブルの名前を付ける前に保存すべきもの(時間枠、ステータス変更、金額など)を示唆します。

読み取り経路も記録してください。読み取りが構造やインデックス設計を決めます。「自分の予約」画面は日付順でステータスで絞る、管理者は顧客名や予約参照で検索できる、拠点別の日次収益、誰がいつ何を変更したかの監査ビュー、などです。

最後に、監査履歴、ソフトデリート、マルチテナント分離、プライバシールール(連絡先詳細の閲覧制限など)といった非機能要件をメモしてください。

これを書き留めてから生成するなら、そのメモが強力なプロンプトになります。何が必須で、何が変わり得て、何が検索可能であるべきかを明確にします。Koder.aiを使うなら、生成前にこれを書いておくとPlanning Modeの効果が高まります。

エンティティと関係を平易な言葉で定義する

テーブルを触る前に、アプリが保存するものを平易に書いてください。繰り返し出てくる名詞をリストアップします:user、project、message、invoice、subscription、file、comment。各名詞がエンティティ候補です。

次に、各エンティティについて「それは何か、なぜ存在するか」を1文で書きます。例:「Projectは、ユーザーが作成して作業をグループ化し、他を招待するためのワークスペースである」。これにより dataitemsmisc のような曖昧なテーブルを避けられます。

所有権は重要な決定で、ほとんどのクエリに影響します。各エンティティについて:

  • 誰が作るか
  • 誰が見られるか
  • 誰が変更できるか
  • オーナーが削除されたときどうするか(保持、移譲、削除)

次にレコードの識別方法を決めます。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_atupdated_at
  • deleted_at(ソフトデリートと復元が本当に必要な場合のみ)
  • created_by(誰が何をしたかの明確な監査が必要な場合)

多対多の関係はほとんどの場合ジョインテーブルにしてください。例えば複数のユーザーが共同作業できるなら app_members を作り、app_iduser_id を持たせ、ペアに一意制約をかけて重複を防ぎます。

バージョン管理が必要なら、最初から不変テーブル(例:app_snapshots)を計画し、各行を app_id で関連付け created_at を持たせます。

データを守る制約を追加する

プランニングモードで始める
スキーマのメモを明確なプロンプトに変えて、スキャフォールド後のリネームを減らします。

制約はスキーマのガードレールです。どのサービス、スクリプト、管理ツールが触っても成り立つべきルールを決めてください。

まずは識別と関係から。すべてのテーブルに主キーが必要で、「belongs to」フィールドは単なる整数ではなく実際の外部キーにしてください。

その後、重複が問題になる箇所に一意性を追加します(例:同じメールのアカウントが二つあることは問題)。

早めに計画しておくべき高価値な制約:

  • Primary keys:UUID か bigint の一貫したスタイルを選ぶ
  • Foreign keys:関係を明示して孤立した行を防ぐ
  • Unique constraints:ビジネス的な識別(メール、ユーザー名)や「これが一つだけ」ルールに使う
  • Check constraintsamount >= 0status IN ('draft','paid','canceled')rating BETWEEN 1 AND 5 のような安価なルール
  • Not null:現実世界で必須のフィールドには適用する

Cascade の振る舞いは計画の効果が大きく出る部分です。実際に人々が何を期待するかを考えてください。顧客が削除されたとき注文が消えるべきではないことが多く、この場合は削除を制限して履歴を残す方が望ましいでしょう。一方、注文の明細のように親なしでは意味がないデータには CASCADE が適していることがあります。

後でモデルとエンドポイントを生成するとき、これらの制約は必須要件になります:どんなエラーを扱うか、どのフィールドが必須か、どのエッジケースが設計上あり得ないかが明確になります。

実際のクエリからインデックスを計画する

インデックスは一つの質問に答えます:実際のユーザーにとって何を速くする必要があるか。

最初に出荷する画面やAPIコールを基準にしてください。フィルタで絞って最新順に並べる一覧ページと、関連レコードを読み込む詳細ページでは必要なインデックスが異なります。

インデックスを選ぶ前に、5〜10件のクエリパターンをプレーンな英語(または平易な言葉)で書き出してください。例:「過去30日間の請求書を表示、支払い済み/未払いでフィルタ、created_atでソート」や「プロジェクトを開いてタスクを due_date で一覧する」など。これがインデックス選びを現実に根ざしたものにします。

よく使われる最初のインデックスセットは、ジョインに使う外部キー、共通のフィルタカラム(statususer_idcreated_at)と、安定した複合フィルタ用に1〜2個の複合インデックスです。例:常に account_id でフィルタして時間でソートするなら (account_id, created_at) のような複合インデックスが有効です。

複合インデックスの順序は重要です。最も選択性が高く、最も頻繁にフィルタされるカラムを先頭に置いてください。例えばテナントIDを毎回フィルタするなら、多くのインデックスで先頭に置くべきです。

「念のために全部インデックスを貼る」は避けてください。各インデックスはINSERTやUPDATE時に追加のコストを生み、稀なクエリを少し速くするために全体の性能を下げることがあります。

全文検索は別に計画してください。単純な「含む」検索で十分なら最初は ILIKE で良いこともありますが、検索が重要なら早めに全文検索(tsvector)を計画しておくと後で設計をやり直さずに済みます。

コード生成前にマイグレーション戦略を決める

スキーマは最初のテーブル作成で「完成」するものではありません。機能追加や誤り修正、データからの学びで変わり続けます。最初にマイグレーション戦略を決めておけば、コード生成後の辛い書き直しを避けられます。

簡単なルール:データベース変更は小さく、機能単位で行ってください。各マイグレーションはレビューしやすく、どの環境でも安全に実行できるべきです。

破壊的変更を安全に扱う方法

多くの破壊的変更はカラムの名前変更・削除や型変更から生じます。一度に全部やるのではなく、安全なステップを計画してください:

  • まず新しいカラムを追加する(必要ならNULL許容)してデータをバックフィルする
  • アプリを稼働させ続ける必要がある場合は短期間の二重書き(old と new に書く)を使う
  • 読み取りを新しいフィールドに切り替え、後で古いカラムを削除する

ステップは増えますが、ダウンタイムや緊急パッチを減らせるので現実的には速く安全です。

シードデータもマイグレーションの一部です。役割、ステータス、国、プラン種別など「常に存在する」参照テーブルを決めて、それらの挿入・更新は専用のマイグレーションに入れておくと、全開発者・全デプロイで一致した状態になります。

ローカル、開発、プロダクションでの一貫性ルール

早めに期待値を設定してください:

  • 同じマイグレーションを同じ順序で実行すること
  • 本番でマッチするマイグレーションなしに手動ホットフィックスを行わないこと
  • 各マイグレーションに前方の説明と現実的なロールバックプランを用意すること

ロールバックが常に完璧な「ダウン」移行であるとは限りません。場合によってはバックアップ復元が最良のロールバックです。Koder.aiを使っている場合は、大きな変更の前にスナップショットを使って素早く戻せる方針を決めておくと便利です。

コピーして調整できるシンプルな例

変更をスナップショットで保護
リスクのあるマイグレーションの前にスナップショットを取り、迅速にロールバックできるようにします。

小さなSaaSアプリを想像してください。人々がチームに参加し、プロジェクトを作り、タスクを追跡します。

まずエンティティと、初日に必要な最低限のフィールドを並べます:

  • users: id, email, full_name, created_at
  • teams: id, org_id, name, created_at
  • team_members: team_id, user_id, role, joined_at
  • projects: id, team_id, name, status, created_at
  • tasks: id, project_id, assignee_user_id (nullable), title, state, due_date (nullable), created_at

関係は明快です:チームは複数のプロジェクトを持ち、プロジェクトは複数のタスクを持ち、ユーザーは team_members を通じてチームに参加します。タスクはプロジェクトに属し、ユーザーに割り当てられる場合があります。

次に、典型的なバグを防ぐいくつかの制約を追加します:

  • users.email を一意にする(case-insensitive にするなら citext を使う)
  • 同じ org 内でのチーム名を一意にする:UNIQUE (org_id, name) を teams に付ける
  • 重複メンバーシップを防ぐ:UNIQUE (team_id, user_id) を team_members に付ける

インデックスは実際の画面に合わせます。例えばタスクリストが projectstate でフィルタして最新順に並べるなら、tasks (project_id, state, created_at DESC) のようなインデックスを計画します。「自分のタスク」が重要なビューなら tasks (assignee_user_id, state, due_date) のようなインデックスが役立ちます。

マイグレーションは最初は安全で地味に:テーブル作成、主キー、外部キー、コアの一意制約。使用が証明された後に追加するもの(soft delete の導入やインデックスの調整など)は次の変更に回しましょう。

よくあるミスと回避法

多くの書き直しは最初のスキーマにルールや実際の使用詳細が欠けていることが原因です。良い計画は完璧な図を作ることではなく、早く罠を見つけることです。

一般的な誤りは重要なルールをアプリケーションコードだけに置いてしまうことです。一意であるべき、必須であるべき、範囲内であるべき値はデータベース側で強制すべきです。さもないとバックグラウンドジョブや新しいエンドポイント、手動インポートがロジックを迂回してしまいます。

インデックスを後回しにするのもよくある見落としです。ローンチ後に追加すると推測ベースになりがちで、本当の遅いクエリがジョインやステータスであるのに別のものをインデックスしてしまうことがあります。

ジョインテーブルも静かなバグの原因です。重複を防ぐ制約がないと同じ関係が二重に保存され、原因究明に時間を使うことになります。

監査ログ、ソフトデリート、イベント履歴が後から必要になることも普通です。それらを後から足すとエンドポイントやレポートに波及します。

最後に、JSONカラムは柔軟性の誘惑がありますが、チェックが外れ、インデックスしにくくなる点に注意してください。コアのビジネスフィールドには向かず、本当に可変なペイロードだけに使うべきです。

生成前のクイック修正リスト:

  • 主要なルールを制約に移す:NOT NULL、CHECK、UNIQUE、外部キー
  • 3〜5件の実際のクエリを書き、そちらに合わせてインデックスを選ぶ
  • ジョインテーブルに複合 UNIQUE を追加(例:user_id + role_id
  • 監査履歴が必要なら専用テーブルでモデル化する
  • JSONは稀で任意の属性に留め、重要なフィールドはカラムにする

モデルやエンドポイント生成前のクイックチェックリスト

まず計画して、より速く生成
エンティティと主要なクエリを書いてから、Koder.aiにコード生成させましょう。

ここで一旦止まり、計画がモデル生成を驚きなしで進められるほど完成しているか確認してください。目標は完璧ではなく、後で書き直しを招く欠落を見つけることです:関係が抜けている、ルールが不明確、インデックスが実際の使い方に合っていない、など。

短いプレフライトチェック:

  • 各エンティティについて、誰が所有するか(user、org、system)と「削除」がどう扱われるか(ハードデリート、ソフトデリート、アーカイブ)を1文で書く
  • すべてのテーブルでカラム型、必須か任意か、デフォルト、タイムスタンプがアプリ由来かDB由来かを確認する
  • 絶対壊してはいけないルールを書く:主キー、外部キー、一意性、簡単なチェック(amount >= 0 や許可ステータス)
  • 上位3〜5の画面・APIコールを選び、正確なフィルタとソート順を書き、インデックスが合っているか確認する
  • 最初のマイグレーションのスケッチ:初期スキーマ、初日に必要なデータ(シード)、すぐに来そうな変更(ステータス追加、名前分割、org導入)

簡易テスト:明日チームメンバーが来て最初のエンドポイントを作ると想定してください。その人が毎時間「これはNULLにして良い?」「削除されたら何が起きる?」と聞かずに済む計画になっていますか?

次のステップ:計画からコードへ、書き直しを減らして進める

計画が読みやすく、主要なフローが紙の上で意味を持つようになったら、それを実行可能なものに変えます:実際のスキーマとマイグレーション。

まず初期マイグレーションでテーブル、型(enumを使うなら)と必須の制約を作ります。最初は小さくても正しいパスを取り、少しのシードデータを入れてアプリで必要なクエリを実行してみてください。フローが違和感あるなら、マイグレーション履歴が短いうちにスキーマを直してください。

生成は、スキーマが安定していて翌日全面的に名前を変える必要がない時に最も速くなります。初期の基本操作(作成、更新、一覧、削除、+1つの実際のビジネスアクション)をスキーマ上でテストしてからモデルとエンドポイントを生成してください。

書き直しを少なく保つ実務ループ:

  • テストから学んだことがあれば計画を更新する
  • 新しいマイグレーションを追加する(他人が使い始めた古いマイグレーションは編集しない)
  • スキーマが変わったらモデルとエンドポイントを再生成する
  • 同じフローとクエリを再実行して何も壊れていないことを確認する
  • コアパスが安定してから便利なフィールドや追加インデックスを入れる

何をDBで検証し、何をAPI層で検証するか早めに決めてください。永続ルール(外部キー、一意制約、チェック制約)はデータベースに入れ、柔らかいルール(フラグ、テンポラリな制限、頻繁に変わる複雑なクロステーブルロジック)はAPI側に置くのが基本です。

Koder.aiを使う場合は、まずPlanning Modeでエンティティとマイグレーションを合意し、その後Go+PostgreSQLバックエンドを生成するのが賢明です。もし変更で問題が起きたら、スナップショットとロールバックで既知の良いバージョンに戻し、スキーマ計画を調整できます。

よくある質問

なぜモデルやエンドポイントを生成する前にPostgresスキーマを計画すべきですか?

スキーマを先に計画してください。それが安定したデータ契約(テーブル、キー、制約)を作り、生成されたモデルやエンドポイントがあとで何度もリネームや書き直しを必要としなくなります。

実務では:エンティティ、関係、主要なクエリを書き出し、コードを生成する前に制約、インデックス、マイグレーションを固めてください。

図に固執せずにスキーマ計画を始める最速の方法は?

アプリが何を記録し、ユーザーがそのデータで何ができるべきかを2〜3文で書いてください。

次に:

  • データを作成/変更するアクション(動詞)
  • 人々が使う画面やレポート(読み取り経路)
  • 監査履歴、ソフトデリート、マルチテナンシー、プライバシーなどの非機能的要件

これだけで表を過剰に作らずに設計を始められます。コレクションや図にこだわりすぎないのが早く進めるコツです。

コアエンティティはどうやって見つけますか?

繰り返し出てくる名詞(user、project、invoice、task など)をリストアップして始めてください。各々について「それは何か、なぜ存在するか」を1文で書きます。

説明できないものは曖昧なテーブル(itemsmisc)になりがちです。明確さがコアエンティティを決める助けになります。

PostgresではUUIDとbigintのどちらを使うべきですか?

スキーマ全体で一貫したID戦略をデフォルトにしてください。

  • UUID:ウェブ/モバイル/バックグラウンドジョブなど多くの場所でレコードが作られるとき、または予測可能なIDを避けたいときに有効
  • bigint:サーバ側で作られることが前提で、サイズが小さく高速

人間向けの識別子が必要なら、主キーとは別に一意のカラム(例:project_code)を追加してください。

外部キーの削除挙動(CASCADE vs RESTRICT)はどう選べばいいですか?

関係ごとに期待される挙動に基づいて決めてください。

一般的なデフォルト:

  • 履歴を残したい場合:親を削除すると重要なレコードが消えるのは避けたいので RESTRICTNO ACTION を使う
  • 子に意味がない場合:親削除で CASCADE を使っても良い(例:注文 → 注文明細)

これは早い段階で決めると、APIの挙動やエッジケースに与える影響が小さくなります。

初日に追加すべき制約は何ですか?

データベースには永続的なルールを入れて、どのライター(API、スクリプト、インポート、管理ツール)も従うようにしてください。

優先順位:

  • テーブルごとに PRIMARY KEY
  • “belongs to” カラムには FOREIGN KEY
  • 重複が問題になるものに UNIQUE(例:メール、ジョインテーブルの (team_id, user_id)
  • 単純なルールに CHECK(非負の金額、許可されたステータスなど)
  • 行の意味に必須なら NOT NULL
過剰にインデックスを付けずに適切に選ぶには?

推測ではなく実際のクエリパターンから始めてください。

5〜10件のプレーンなクエリ(フィルタ+ソート)を書き出し、それに合わせてインデックスを決めます:

  • ジョインで使う外部キー
  • statususer_idcreated_at のような共通フィルタ
  • 安定した複合クエリ向けの複合インデックス(例:(account_id, created_at)

すべてにインデックスを貼ると挿入や更新が遅くなるので避けてください。

多対多の関係はどうモデル化すべきですか?

ジョインテーブルを作り、2つの外部キーと複合一意制約を追加してください。

例:

  • team_members(team_id, user_id, role, joined_at)
  • UNIQUE (team_id, user_id) を追加して重複を防ぐ

これで「なぜこのユーザーが二回見えるのか?」のような微妙なバグを避けられます。

Postgresでの良い型やカラムパターンのデフォルトは?

デフォルト:

  • タイムスタンプは timestamptz(タイムゾーンの驚きを減らす)
  • 金額は numeric(12,2) やセントを整数で保存(浮動小数点は避ける)
  • ステータス値は Postgres の enum または CHECK で強制する

同じ概念には同じ型を使うことで、結合やバリデーションが予測可能になります。

生成したコードが後で壊れないためのマイグレーション戦略は?

小さくてレビューしやすいマイグレーションを使い、一度に壊れる変更を避けます。

安全な手順:

  • まず新しいカラムを追加(必要ならNULL許容)
  • バックフィル
  • 必要なら短期間だけ二重書き(old/new 両方へ書く)
  • 読み取りを新しいカラムに切り替える
  • 後で古いカラムを削除

シード/参照データもマイグレーションに含めて、全環境で同じ結果になるようにしてください。

Related posts