Database architecture and PostgreSQL best practices. Use when designing schemas, writing migrations, creating indexes, optimising queries, or making technology selection decisions. Covers schema design, indexing strategies, JSONB usage, partitioning, and performance patterns.
Database architecture principles and PostgreSQL-specific best practices. Apply these rules when designing schemas, writing queries, or planning data infrastructure.
snake_case for all identifiers: tables, columns, indexes, constraintsusers, order_items, audit_logs<referenced_table_singular>_id: user_id, order_ididx_<table>_<columns>: idx_orders_user_id_created_at<type>_<table>_<columns>: uq_users_email, chk_orders_total_positiveTIMESTAMPTZ always — never TIMESTAMP without time zoneNUMERIC(p,s) — never FLOAT or DOUBLE PRECISION for monetary valuesTEXT over VARCHAR(n) unless a hard length constraint is genuinely requiredBIGINT GENERATED ALWAYS AS IDENTITY for internal IDs; UUID (v7 preferred) for public-facing IDsBOOLEAN with a NOT NULL constraint and explicit defaultTEXT with a CHECK constraint, not CREATE TYPE enum (enums are painful to modify)| Use Case | Index Type |
|---|---|
| Equality and range queries | B-tree (default) |
| JSONB containment, arrays, full-text search | GIN |
| Range types, geometric/spatial data | GiST |
| Large append-only / time-series tables | BRIN |
CREATE INDEX idx_orders_active ON orders(status) WHERE status != 'archived'INCLUDE) for index-only scans on frequently read columnspg_stat_user_indexes to find unused indexesCREATE INDEX idx_events_data ON events USING gin(data)@>) over path extraction (->>) for indexed lookupsCHECK constraintsCREATE INDEX CONCURRENTLY to avoid locking tables during index creation (cannot run inside a transaction)now()) on large tables — it triggers a full table rewrite. Add the column as nullable, backfill, then set the default| Strategy | Use Case |
|---|---|
RANGE | Time-series data (partition by month/week) |
LIST | Discrete categories (tenant, region, status) |
HASH | Even distribution when no natural range/list key exists |
WHERE clauses for partition pruning(2 * CPU cores) + effective_spindle_count on the database server as a starting pointidle_in_transaction_session_timeout to prevent abandoned transactions holding locksUNLOGGED tables for ephemeral staging data (not crash-safe — data lost on crash)COPY or multi-row INSERT ... VALUES rather than single-row loopsEXPLAIN (ANALYZE, BUFFERS) on any query over 50 msEXISTS over COUNT(*) when checking for presenceSELECT * — specify columns explicitlyStack-specific patterns: See
resources/for ORM-specific implementation details, migration tooling, and framework conventions.