このページは英語でのみご覧いただけます。
PostgreSQL Schema & Migrations
Pathruleルール3件 • メモリ2件 • スキル1件
A pattern bundle that teaches AI agents how to model PostgreSQL tables with correct types and constraints, and how to evolve them without downtime. It enforces forward-only, expand-contract migrations, concurrent index builds, and validated constraints so schema changes never lock production traffic. Tuned for PostgreSQL 18 with native UUIDv7 and identity columns.
推奨されるパスの配置
Pathruleは各要素を対応するパスに配置するため、アシスタントはそれが属する場所でだけ参照します。これはインポート時に適用されるスコープで、ワークスペースで調整できます。
ルール
3Migrations are forward-only, no down steps/db/migrations高強制Never author down/rollback migrations; recover by shipping a new forward migration.
| 1 | Every migration moves the schema forward only. There are no `down` or rollback steps in this codebase. |
| 2 | |
| 3 | - Do not write a `down`, `rollback`, or reverse step in any migration. A migration that has touched production data cannot be cleanly reversed, and a `down` that drops a column destroys data the previous app version may still need. |
| 4 | - Recover from a bad migration by shipping a new forward migration that corrects the state, not by reverting. |
| 5 | - Keep each migration small and single-purpose so a follow-up fix is easy to reason about. |
| 6 | - Migrations are immutable once merged. Never edit a migration file that has already run anywhere; add a new one. |
DDL on populated tables must be lock-safe/db/migrations高強制Use CONCURRENTLY, lock_timeout, and NOT VALID so DDL never takes a long ACCESS EXCLUSIVE lock on a hot table.
| 1 | DDL against a table that already holds production rows must acquire only weak, short locks, so a deploy never blocks live traffic behind an `ACCESS EXCLUSIVE` lock. |
| 2 | |
| 3 | - Build and drop indexes with `CREATE INDEX CONCURRENTLY` / `DROP INDEX CONCURRENTLY`. These take only a `SHARE UPDATE EXCLUSIVE` lock but cannot run inside a transaction block, so the migration tool must run that statement outside any wrapping transaction. |
| 4 | - Set `SET lock_timeout = '5s'` (or similar) before DDL on a populated table. A statement that cannot get its lock then fails fast instead of queuing every subsequent query behind it. |
| 5 | - Add foreign keys and `CHECK` constraints as `NOT VALID` first, then run `VALIDATE CONSTRAINT` in a separate statement. The `NOT VALID` step takes a brief strong lock to add the catalog entry; `VALIDATE` scans the table under a weaker `SHARE UPDATE EXCLUSIVE` lock that allows concurrent reads and writes. |
| 6 | - Add a unique constraint without a long lock by running `CREATE UNIQUE INDEX CONCURRENTLY`, then `ALTER TABLE ... ADD CONSTRAINT ... UNIQUE USING INDEX <name>`. |
| 7 | - If a `CREATE INDEX CONCURRENTLY` fails, it leaves an `INVALID` index behind; drop it (`DROP INDEX CONCURRENTLY`) before retrying. |
Evolve columns with expand-contract, never in place/db/migrations高強制Rename and retype across multiple deploys so old and new app versions both keep working.
| 1 | Schema changes that affect existing data run as separate expand, backfill, and contract migrations so every deployed app version stays compatible during a rolling deploy. |
| 2 | |
| 3 | - Never `RENAME` or `ALTER ... TYPE` a column in a single step while the old app version is still running. Add the new column, dual-write from the app, backfill, switch reads to the new column, then drop the old column in a later deploy. |
| 4 | - Add new columns as nullable or with a NON-volatile constant `DEFAULT`. PostgreSQL stores a constant default in catalog metadata (`pg_attribute.attmissingval`) so the `ALTER TABLE` is instant with no table rewrite. A VOLATILE default (such as `clock_timestamp()`), a stored generated column, or an identity column DOES force a full rewrite under a strong lock; avoid those on large populated tables. |
| 5 | - Backfill large tables in bounded batches (for example a few thousand rows per statement), each batch in its own transaction, not one giant `UPDATE` that holds locks and bloats WAL. |
| 6 | - Apply `NOT NULL` on a backfilled column safely: add a `CHECK (col IS NOT NULL) NOT VALID`, `VALIDATE` it, then `SET NOT NULL` (which can use the validated check to skip a scan), instead of `SET NOT NULL` directly on a large table. |
| 7 | - Drop the old column or table only after every running app version has stopped reading and writing it. |
メモリ
2Default column types for new tables (PostgreSQL 18)/dbThe canonical type and key choices for new PostgreSQL 18 tables and why.
| 1 | Pick the same correct types on every new table so the schema stays consistent and bug-free on PostgreSQL 18. |
| 2 | |
| 3 | - Primary keys: `bigint GENERATED ALWAYS AS IDENTITY` for internal rows, or `uuid DEFAULT uuidv7()` (native function added in PG 18) when ids are exposed externally or generated client-side. UUIDv7 is time-ordered so it indexes far better than random UUIDv4. Never use `serial`/`bigserial` (they leave the sequence ownership and grants in a surprising state and are effectively legacy). |
| 4 | - Timestamps: always `timestamptz`, never `timestamp` (without time zone). `timestamptz` records a single absolute moment; `timestamp` silently drops the offset and causes timezone bugs. Default audit columns to `now()`. |
| 5 | - Strings: use `text`. `text` and `varchar(n)` share identical storage and performance in PostgreSQL; `varchar(n)` only adds a length check. Enforce a real maximum with a `CHECK` constraint when the limit is a genuine business rule, not a guess. |
| 6 | - Money and exact decimals: `numeric`, never `float`/`double precision` (binary floats cannot represent decimal cents exactly). Use `boolean` for flags and `jsonb` (not `json`) for semi-structured data, since `jsonb` is indexable and `json` only stores reparsed text. |
| 7 | |
| 8 | See /db/migrations for the lock-safe and expand-contract rules that govern how these tables are changed after creation. |
Index and constraint design checklist/dbWhere indexes and constraints belong, and how to add unique/FK safely.
| 1 | Model integrity at the database, and index for the queries that actually run. |
| 2 | |
| 3 | - Index every foreign key column on the CHILD side. PostgreSQL does NOT create that index automatically, and without it a delete or update of a parent row scans the whole child table to check references, which is the classic cause of mysteriously slow parent deletes and `ON DELETE CASCADE` operations. |
| 4 | - Use partial indexes (for example `WHERE deleted_at IS NULL`) and expression indexes when queries only filter a subset or a computed value, instead of indexing the whole column. Smaller indexes mean less write amplification and a higher chance the planner uses them. |
| 5 | - Enforce business rules with `NOT NULL`, `UNIQUE`, `CHECK`, and foreign keys in the schema rather than trusting application code; the database is the only layer every writer goes through. |
| 6 | - Avoid redundant indexes: a B-tree on `(a, b)` already serves queries filtering on `a` alone, so a separate index on `(a)` is usually wasted write cost. |
| 7 | - For low-cardinality columns the planner will ignore the index, so do not add one just because a column is in a WHERE clause. |
| 8 | |
| 9 | See /db/migrations for how to add these indexes and constraints concurrently without locking production. |
スキル
1postgres-schema-review/rootPre-merge checklist for any PostgreSQL schema change or migration.
| 1 | --- |
| 2 | name: postgres-schema-review |
| 3 | description: Review a PostgreSQL schema change or migration before merge. Use when adding or altering tables, columns, indexes, or constraints, or when writing a migration file, to confirm correct types, sound integrity, and lock-safe forward-only DDL on PostgreSQL 18. |
| 4 | --- |
| 5 | |
| 6 | # PostgreSQL schema and migration review |
| 7 | |
| 8 | ## Types and keys |
| 9 | |
| 10 | - [ ] Primary key is `bigint GENERATED ALWAYS AS IDENTITY` or `uuid DEFAULT uuidv7()`, not `serial`/`bigserial`. |
| 11 | - [ ] All point-in-time columns are `timestamptz`, not `timestamp`. |
| 12 | - [ ] Strings use `text` (length enforced via `CHECK` only when a real limit exists); money/decimals use `numeric`; structured data uses `jsonb`, not `json`. |
| 13 | |
| 14 | ## Integrity and indexes |
| 15 | |
| 16 | - [ ] `NOT NULL`, `UNIQUE`, `CHECK`, and foreign keys express the real business rules at the database level. |
| 17 | - [ ] Every foreign key column has a covering index on the child side. |
| 18 | - [ ] Partial or expression indexes are used where queries filter a subset, instead of broad full-column indexes; no redundant indexes were added. |
| 19 | |
| 20 | ## Forward-only |
| 21 | |
| 22 | - [ ] Migration has no `down`/rollback step. |
| 23 | - [ ] No already-merged migration file was edited; this is a new file. |
| 24 | |
| 25 | ## Lock-safe DDL |
| 26 | |
| 27 | - [ ] `lock_timeout` is set before DDL on any populated table. |
| 28 | - [ ] Indexes are built/dropped `CONCURRENTLY`, and that statement runs outside a transaction block. |
| 29 | - [ ] New foreign keys and check constraints are added `NOT VALID`, then `VALIDATE`d in a separate statement. |
| 30 | - [ ] New columns are nullable or use a NON-volatile constant `DEFAULT` so no table rewrite is triggered (no volatile default, identity, or stored generated column added to a large table). |
| 31 | - [ ] A unique constraint is added via `CREATE UNIQUE INDEX CONCURRENTLY` + `ADD CONSTRAINT ... USING INDEX`. |
| 32 | |
| 33 | ## Expand-contract |
| 34 | |
| 35 | - [ ] Renames and type changes are split into expand, backfill, and contract deploys; nothing is renamed/retyped in place. |
| 36 | - [ ] Backfills run in bounded batches, each in its own transaction. |
| 37 | - [ ] `NOT NULL` on a backfilled column is applied via a validated `CHECK`, not a direct `SET NOT NULL` scan on a large table. |
| 38 | - [ ] Old columns or tables are dropped only after no running app version uses them. |
このパターンを使う理由
AI agents write migrations that rename columns in place, build indexes that lock the table, and pick types that cause silent data and timezone bugs.
対象:Backend and platform teams running PostgreSQL in production
アシスタントが次のことをしないようにします。
- Blocking ACCESS EXCLUSIVE locks from rewriting tables or building indexes inline during deploys
- Destructive in-place column renames or retypes that break the previous app version mid-rollout
- Wrong column types like timestamp without time zone, varchar(n), or serial that cause timezone and overflow bugs
- ライセンス
- Apache-2.0
- バージョン
- 1.0.1
- 更新日
- 2026-08-25