1---2name: postgres3description: Use for PostgreSQL regardless of ORM — schema design, queries, indexing, EXPLAIN/plans, JSONB, partitions, extensions, migrations, security. Triggers — psql, SQL DDL on a Postgres stack.4---56# PostgreSQL Development78## When to use9- Designing or reviewing table schemas, constraints, and indexes10- Writing or optimising complex SQL queries, CTEs, or window functions11- Authoring or debugging database migrations12- Configuring connection pooling, vacuuming, replication, or backups13- Diagnosing slow queries with `EXPLAIN (ANALYZE, BUFFERS)`14- Implementing row-level security, roles, or audit logging1516## Workflow17181. **Understand the access patterns first** — what queries will run most frequently and at what volume? Schema design follows query design, not the reverse.192. **Design the schema**:20 - Choose the correct data types (avoid `TEXT` where `VARCHAR(n)` or a domain type is better; use `TIMESTAMPTZ` not `TIMESTAMP`; use `UUID` or `BIGSERIAL` for PKs).21 - Add constraints early: `NOT NULL`, `UNIQUE`, `CHECK`, foreign keys with `ON DELETE` policy.22 - Normalise to 3NF by default; denormalise only when a proven performance need exists with a comment explaining why.233. **Create indexes deliberately**:24 - Single-column B-tree for equality and range filters on high-cardinality columns.25 - Composite index column order: most selective equality columns first, then range columns.26 - Partial indexes for sparse conditions: `CREATE INDEX ON orders (user_id) WHERE status = 'pending'`.27 - GIN for `jsonb`, full-text search, and array containment.284. **Write the migration**:29 - One migration file per logical change with an `up` and `down` (or an explicit comment if rollback is destructive).30 - Never add a `NOT NULL` column without a `DEFAULT` in the same statement on a live table — it rewrites the full table pre-PG11.31 - Add indexes `CONCURRENTLY` on production tables to avoid locking.325. **Write queries**:33 - Parameterise all user input — never string-interpolate into SQL.34 - Use CTEs for readability; materialise with `MATERIALIZED` only when the planner is misestimating.35 - Prefer `JOIN` over correlated subqueries in `SELECT` list.366. **Profile slow queries**:37 - `EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)` on the exact query with real parameters.38 - Look for: `Seq Scan` on large tables, high `Rows Removed by Filter`, `Hash Batches > 1` (spill to disk), nested loops with large outer sets.39 - Run `pg_stat_statements` to find top-N slow queries by total time.407. **Audit for security** — see .claude/checklists/security.md. Row-level security, principle of least privilege on roles, encrypted connections.418. **Verify backup/restore** before going live — a backup that has never been restored is an untested backup.4243## Standards4445### Schema design46- Primary keys: `BIGSERIAL` for append-heavy tables; `UUID` (`gen_random_uuid()`) for distributed or externally referenced entities.47- Always `TIMESTAMPTZ` for timestamps — store in UTC, display in application layer.48- Foreign keys must have an index on the referencing column unless they are almost never queried by FK.49- Use `ENUM` types or a lookup table for finite, stable sets of values; use `CHECK (status IN (...))` for small, unlikely-to-change sets.5051### Migrations52- Migrations are immutable once merged — never edit a committed migration; write a new one.53- Test `up` and `down` migrations in CI against a real Postgres container.54- Large table changes (adding a column, changing a type): do in multiple small migrations with no-downtime patterns (expand/contract).55- Never `DROP TABLE` or `DROP COLUMN` in the same deployment as the code that stops using it — wait one release.5657### Queries58- Use `RETURNING` to get generated IDs/timestamps in a single round trip instead of a follow-up `SELECT`.59- `LIMIT` + `OFFSET` pagination degrades at high offsets; use keyset pagination (`WHERE id > $last_id ORDER BY id LIMIT $n`).60- `COUNT(*)` is fast; `COUNT(DISTINCT col)` on large tables is slow — consider HyperLogLog via `pg_hll` for approximations.61- Wrap multi-step mutations in explicit transactions with appropriate isolation level (`READ COMMITTED` default; `REPEATABLE READ` for read-modify-write cycles).6263### Performance64- `autovacuum` must be healthy: check `pg_stat_user_tables.n_dead_tup`. Tune `autovacuum_vacuum_scale_factor` for large tables.65- Connection pooling is mandatory at scale — use PgBouncer (transaction mode) or `pgpool-II`; never open one Postgres connection per application thread.66- `shared_buffers` = 25% of RAM; `effective_cache_size` = 75% of RAM; `work_mem` = RAM / (max_connections × 2) as a starting point.6768### Security69- Application user has `SELECT`, `INSERT`, `UPDATE`, `DELETE` on required tables only — never `SUPERUSER` or schema-owner.70- Enable `ssl = on`; require `hostssl` in `pg_hba.conf`.71- Never store plain-text passwords; store argon2/bcrypt hashes.72- Use Row-Level Security (`ALTER TABLE ... ENABLE ROW LEVEL SECURITY`) for multi-tenant data.7374### Do not75- Do not use `SELECT *` in application queries — always list columns explicitly.76- Do not run `VACUUM FULL` or `REINDEX` without a maintenance window — they take `AccessExclusiveLock`.77- Do not create indexes without profiling first — every index slows writes.78- Do not use `serial` / `bigserial` for new projects — use `GENERATED ALWAYS AS IDENTITY` (SQL standard).79- Do not share a database superuser account in application connection strings.8081## Common mistakes to avoid8283| Mistake | Fix |84|---|---|85| Adding a `NOT NULL` column to a large live table | Use `ADD COLUMN col TYPE DEFAULT val`, then backfill, then add `NOT NULL` in a later migration (PG<11). PG11+ handles this in one DDL. |86| Index not used despite existing | Check column order, data type mismatch, or function wrapping in WHERE clause (`WHERE lower(email) = ?` needs functional index). |87| `LIKE '%term%'` not using index | Use `pg_trgm` GIN index: `CREATE INDEX ON t USING gin (col gin_trgm_ops)`. |88| Long-running transaction blocking autovacuum | Set `statement_timeout` and `idle_in_transaction_session_timeout` in `postgresql.conf`. |89| JSONB overuse replacing relational columns | Use JSONB for truly variable/schemaless attributes; model known fields as typed columns. |90| Missing `FOR UPDATE` in optimistic lock patterns | Use `SELECT ... FOR UPDATE` or `UPDATE ... WHERE version = $v` with row count check. |9192## Output format9394- Schema change: `CREATE TABLE` or `ALTER TABLE` DDL with all constraints, followed by `CREATE INDEX` statements.95- Migration file: numbered file (`YYYYMMDDHHMMSS_description.sql`) with `-- migrate:up` and `-- migrate:down` sections.96- Query optimisation: original query, `EXPLAIN ANALYZE` snippet of the problem node, rewritten query, and expected improvement.97- Role/permission setup: `CREATE ROLE`, `GRANT`, `REVOKE` statements with comments on why each privilege is granted.9899## Related checklists100- .claude/checklists/security.md101- .claude/checklists/performance.md102- .claude/checklists/qa.md103104## Related agents105- .claude/agents/core/orchestrator.md106- .claude/agents/engineering/database-architect.md