PostgreSQL 規約
適用方法
2つのモードがあります。何かを書く前にどちらが該当するかを判断してください。
モード A — あなたが名前を決める。 ユーザーがスキーマ、エンティティ、マイグレーションを依頼し、名前を指定しなかった場合。以下の規約を直接適用します。
モード B — 名前がすでに存在するか、指定されている。 ユーザーが名前を指定した場合、または既存のコード、マイグレーション、データベースにこれらの規約から逸脱する名前が含まれている場合。決して静かにリネームせず、報告して確認を取ってください。
変更前の報告形式(ユーザーの言語で):
命名:
Must fix
- `"UserProfile"` (テーブル) → `user_profiles` — 引用符付きPascalCaseは以後すべてのクエリで引用が必要
- `created` timestamp DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC') → `created_at timestamptz DEFAULT now()` — セッションのTimeZoneがUTCでない場合、値が変動します
推奨
- `users.user_email` → `users.email` — テーブル名がすでにコンテキストを提供
次に1つ質問します:すべて適用、「Must fix」のみ適用、または現状維持。回答を待ってください。
報告ルール:
- Must fix = バグ、クエリ破壊、ツールの問題を引き起こすもの:引用符付きの混合ケース識別子、予約語、63バイトを超える名前、タイムゾーンなしの
timestamp、誤った時刻デフォルト値、jsonb/numericが必要なデータに対するjson/float/money、FKインデックス欠落、トランザクション外のロック。 - 推奨 = スタイルと一貫性。
- 現在のタスクで触れるオブジェクトのみ報告してください。他の場所で逸脱に気づいた場合、「<場所>にも同様の逸脱がN件 — 一覧表示しますか?」と1行追加します。
- 適用済みマイグレーションまたは実稼働データベース内のオブジェクト:リネームは破壊的変更です。提案のみ行い、承認された場合は新しいマイグレーションとして実装します。適用済みマイグレーションを編集しないでください。
- コードベースが一貫して異なる規約を使用している場合、その規約が優先されます。その違いを一度だけ指摘し、以降はプロジェクトの規約に従い、各出現をフラグ付けしないでください。
- ORMの命名規約(例:EFCore.NamingConventions)で生成された名前はそのまま受け入れられます。
- ユーザーが規約外の名前を確認したら、それを使用し、このセッションで再度指摘しないでください。
一般ルール
snake_case、小文字、引用符なしの識別子。- 識別子は最大63バイト。それより長い名前は静かに切り詰められます。単に切り詰めるのではなく、意味を保ったまま短縮してください。
- テーブル名は複数形:
users、orders、order_items。 - カラム名は単数形、テーブル接頭辞なし:
users.email、users.user_emailではありません。 - 一般的な略語を除き、完全な単語を使用:
id、url、ip、sku。 - 予約語や曖昧な単語を名前として使用しない:
user、order、group、table、limit、offset、desc、end、date、time、timestamp。 - 型やオブジェクトの接頭辞なし:
tbl_users、str_nameは禁止。 - ブール値は肯定形:
is_deleted、is_not_deletedは禁止。
標準カラム
| 用途 | 名前 | 型 |
|---|---|---|
| 主キー | id | bigint generated always as identity または uuid v7 |
| 作成日/更新日 | created_at、updated_at | timestamptz not null default now() |
| ソフトデリート | deleted_at | timestamptz null |
| 監査 | created_by、updated_by | users.id へのFK |
| フラグ | is_active、is_verified、has_children、can_edit | boolean not null default false |
| ステータス | status | text + チェック制約、列挙型、またはルックアップテーブル |
| 日付のみ | birth_date、due_on | date |
| 有効期間 | expires_at、valid_from、valid_to | timestamptz |
| 金額 | amount、unit_price、total_amount + currency_code | numeric(19,4) + char(3)、または amount_minor bigint |
| カウンター | items_count、retry_count | integer / bigint |
| 順序 | sort_order | integer |
| 業務キー | code、slug、external_id | text + ユニーク |
| テナント | tenant_id | FK |
| 楽観的ロック | version またはシステム xmin | integer / xid |
| 自由形式データ | metadata、payload、settings | jsonb |
接尾辞には意味があります:_at は瞬間、_on/_date はカレンダー日付、_id は参照、_count はカウンター、_url、_path、_hash。
キーと制約
| オブジェクト | パターン | 例 |
|---|---|---|
| 主キー | pk_<テーブル> | pk_orders |
| 外部キー | fk_<テーブル>_<参照テーブル>_<カラム> | fk_orders_customers_customer_id |
| ユニーク制約 | uq_<テーブル>_<カラム> | uq_users_email |
| 代替キー (EF Core) | ak_<テーブル>_<カラム> | ak_products_sku |
| チェック制約 | ck_<テーブル>_<ルール> | ck_orders_amount_positive |
| 除外制約 | ex_<テーブル>_<カラム> | ex_bookings_room_id_period |
| インデックス | ix_<テーブル>_<カラム> | ix_orders_customer_id_created_at |
| ユニークインデックス | ux_<テーブル>_<カラム> | ux_users_email |
| 部分インデックス | ix_<テーブル>_<カラム>_<条件> | ix_users_email_not_deleted |
| メソッド別インデックス | ix_<テーブル>_<カラム>_<メソッド> | ix_products_tags_gin |
| トリガー | trg_<テーブル>_<アクション> | trg_orders_set_updated_at |
| トリガー関数 | 動詞 + オブジェクト | set_updated_at() |
| シーケンス | PostgreSQL デフォルト | orders_id_seq |
- FKカラム:
<参照テーブル(単数形)>_id、または役割が重要な場合は役割名:author_id、sender_id、recipient_id、parent_id。 - FKパターンにはカラムが含まれるため、1つのテーブルへの複数のFKは一意になります:
fk_messages_users_sender_id、fk_messages_users_recipient_id。 - PostgreSQLはFKカラムにインデックスを作成しません。すべてのFKには、複合インデックスが既にそのカラムで始まっていない限り、インデックスが必要です。EF Coreは規約でこれらを作成しますが、Drizzle、Prisma、TypeORM、Knex、sqlcは作成しません。
リレーション
| 種類 | 形状 | 例 |
|---|---|---|
| 一対多 | 子にFK | order_items.order_id → orders.id |
| 一対一 | FK + ユニーク、または PK = FK | user_profiles.user_id |
| 多対多 | 結合テーブル <a>_<b>、複合PK | user_roles (user_id, role_id)、pk_user_roles |
| 属性付き多対多 | 意味のあるエンティティ名 | memberships、enrollments |
| 自己参照 | parent_id または役割名 | categories.parent_id、employees.manager_id |
ポリモーフィックペア(commentable_type + commentable_id)はFKを持つことができません。代わりに、正確に1つが設定されることをチェック制約で保証する別々のNULL許容FKカラム、または別々のテーブルを推奨します。
スキーマ、ビュー、関数、型
| オブジェクト | パターン | 例 |
|---|---|---|
| スキーマ | ドメインまたはモジュール | auth、billing、catalog、audit |
| ビュー | v_<名前> | v_active_users |
| マテリアライズドビュー | mv_<名前> | mv_daily_sales |
| 関数 / プロシージャ | 動詞 + オブジェクト | get_user_balance、calculate_order_total |
| 列挙型 | <エンティティ>_<属性> | order_status |
| 列挙値 | 小文字のsnake_case | pending、paid、refunded |
| データベースロール | <アプリ>_<アクセス> | shop_app_rw、shop_app_ro、shop_migrator |
列挙値は型を再作成せずに削除できません。変更される可能性のあるセットには、text + チェック制約またはルックアップテーブルを推奨します。
型とデフォルト値
- 瞬間を表すには
timestamptz、デフォルトはnow()。timestamptzカラムに対するCURRENT_TIMESTAMP AT TIME ZONE 'UTC'はバグです。 - 長さがビジネスルールでない限り、
varchar(n)の代わりにtextを使用。長さ制約が必要な場合はcheck (char_length(col) <= n)を推奨。 jsonb、決してjsonを使用しない。- 金額には
numeric、決してreal、double precision、moneyを使用しない。 - 新しいIDカラムには
generated always as identityを使用し、serial/bigserialは使用しない。 - UUID主キーにはv7を使用:PostgreSQL 18
uuidv7()、.NET 9Guid.CreateVersion7()、Nodeuuidパッケージv7()、Gogithub.com/google/uuiduuid.NewV7()。ランダムなv4キーはB-treeインデックスを断片化します。 gen_random_uuid()はPostgreSQL 13以降に組み込まれています。そのためにuuid-osspやpgcryptoは必要ありません。- 大文字小文字を区別しない一意性には
citextまたはlower(email)のユニークインデックスを使用。
並行性とクエリパターン
- 楽観的並行性制御、EF Core + Npgsql:
IsRowVersion()で設定されたuintプロパティはxminにマップされます。 - 楽観的並行性制御、NodeおよびGo:
version integer not null default 0、where id = $1 and version = $2およびversion = version + 1で更新。影響を受けた行が0件の場合は競合を意味します。sqlcでは:execrowsを使用して影響を受けた行数を取得します。 select ... for updateおよびfor update skip lockedは明示的なトランザクション内でのみ使用。自動コミットではロックは即座に解放されます。- アドバイザリロック:トランザクション内で
pg_try_advisory_xact_lock。セッションレベルのpg_advisory_lockはアンロックまで保持される専用コネクションでのみ使用。コネクションプーリングでは、ロックとアンロックが異なるコネクションに行われる可能性があります。 - 大規模または無限リストにはキーセットページネーションを使用:
where (created_at, id) < ($1, $2) order by created_at desc, id desc limit $3。offsetは小規模で上限のあるリストのみ。 - 将来のテーブルに対する権限は、
grant ... on all tablesだけでなく、alter default privilegesが必要です。
ORMのsnake_case設定
| スタック | 設定 |
|---|---|
| EF Core + Npgsql | EFCore.NamingConventions、UseSnakeCaseNamingConvention() |
| Drizzle | casing: 'snake_case' |
| Prisma | フィールドに@map、モデルに@@map |
| TypeORM | typeorm-naming-strategiesのSnakeNamingStrategy |
| Knex / Objection | knexSnakeCaseMappers() |
| Go + sqlc | 不要。Goの名前はsnake_caseから自動生成されます |
プロジェクトがsnake_case設定なしでORMを使用している場合、最初のマイグレーション前であればMust fixとして報告。マイグレーションが存在する場合は、マイグレーションコストを含めて推奨として報告します。
Go + sqlc
sqlcはSQLファーストです。スキーマはマイグレーションファイルから取得されるため、上記のすべてのルールが直接適用されます。
sqlc.yamlのschemaをマイグレーションディレクトリに指定します(goose、golang-migrate、dbmate、atlas形式をサポート。ダウンマイグレーションは無視されます)。マイグレーションの横に別途手書きのスキーマファイルを保持しないでください。- 複数形のsnake_caseテーブルは、単数形のGo構造体を生成します:
order_items→OrderItem。列挙型order_status→ 型OrderStatusと定数OrderStatusPending。 sql_package: pgx/v5を使用します。uuid→github.com/google/uuid.UUID、必要に応じてtimestamptz→time.Time(pgtypeラッパーの代わり)をオーバーライドします。- 頭字語は
rename(url: URL、ip: IP)で修正します。カラム名をリネームしないでください。 - クエリ名はPascalCaseの動詞 + オブジェクトで、結果の種類を含めます:
| 目的 | 例 |
|---|---|
| 1行 | -- name: GetUserByEmail :one |
| 複数行 | -- name: ListOrdersByCustomer :many |
| 挿入 | -- name: CreateOrder :one + returning * |
| 更新 | -- name: UpdateOrderStatus :execrows |
| 削除 | -- name: DeleteSession :exec |
| 一括ロード | -- name: CreateOrderItems :copyfrom |
- パラメータは
@nameまたはsqlc.arg(name)で名前を指定し、NULL許容のものはsqlc.narg(name)を使用。位置指定の$1はColumn1スタイルのGoフィールドを生成します。 - 結合には
sqlc.embed(table)を使用し、アドホックなエイリアスでカラムを選択するのは避けてください。 - スキーマ変更に耐える必要があるクエリでは
select *を使用しないでください。ただし、書き込み時のreturning *は例外です。