postgresql-table-design
Use this skill when designing or reviewing a PostgreSQL-specific schema. Covers best-practices, data types, indexing, constraints, performance patterns, and advanced features
By wshobson · 25,323 installs
npx skills add wshobson/agents --skill postgresql-table-design
Source repository · Upstream listing
PostgreSQL Table Design
When to Use
Designing a new PostgreSQL schema, or reviewing one before it ships.
Choosing column types, keys, constraints, or indexes for PostgreSQL specifically.
Deciding whether and how to partition a large table, or how to store semi structured data.
Planning a schema change on a live database without downtime.
The rules and decision points for a PostgreSQL schema. The full data type catalog, workload
patterns (update heavy, insert heavy, upsert, schema evolution), extensions, JSONB indexing,
and worked DDL examples are in references/details.md ; open it when a section below points there.
Core Rules
Define a PRIMARY KEY for reference tables (users, orders, etc.). Not always needed for time series/event/log data. When used, prefer BIGINT GENERATED ALWAYS AS IDENTITY ; use UUID only when global uniqueness/opacity is needed.
Normalize first (to 3NF) to eliminate data redundancy and update anomalies; denormalize only for measured, high ROI reads where join performance is proven problematic.
Add NOT NULL everywhere it is semantically required; use DEFAULT s for common values.
Create indexes for access paths you actually query : PK/unique (auto), FK columns (manual!) , frequent filters/sorts, and join keys.
Prefer TIMESTAMPTZ for event time; NUMERIC for money; TEXT for strings; BIGINT for integers; DOUBLE PRECISION for floats (or NUMERIC for exact decimal arithmetic).
PostgreSQL Gotchas
Identifiers : unquoted → lowercased. Avoid quoted/mixed case names; use snake case .
Unique + NULLs : UNIQUE allows multiple NULLs. Use UNIQUE NULLS NOT DISTINCT (...) (PG15+) to restrict to one NULL.
FK indexes : PostgreSQL does not auto index FK columns. Add them.
No silent coercions : length/precision overflows error out (no truncation). Inserting 999 into NUMERIC(2,0) fails, unlike databases that silently truncate or round.
Sequences/identity have gaps (normal; don't "fix"). Rollbacks, crashes, and concurrent transactions leave gaps (1, 2, 5, 6...).
Heap storage : no clustered PK by default; CLUSTER is a one off reorganization, not maintained on later inserts.
MVCC : updates/deletes leave dead tuples; vacuum handles them—design to avoid hot wide row churn.
Data Types
IDs : BIGINT GENERATED ALWAYS AS IDENTITY ; UUID for distributed or opaque IDs, generated with uuidv7() (PG18+) or gen random uuid() .
Numbers : BIGINT unless storage is critical; DOUBLE PRECISION over REAL ; NUMERIC(p,s) for money and exact decimals.
Strings : TEXT , with CHECK (LENGTH(col) <= n) when a limit is needed; BYTEA for binary. Case insensitive lookups: expression index on LOWER(col) , or CITEXT when a constraint must be case insensitive.
Time : TIMESTAMPTZ , DATE , INTERVAL . now() is transaction start; clock timestamp() is wall clock.
Booleans : BOOLEAN NOT NULL unless tri state is required.
Enums : CREATE TYPE ... AS ENUM only for small, stable sets; evolving business values get TEXT + CHECK or a lookup table.
JSONB over JSON, indexed with GIN, for optional/semi structured attributes only.
Arrays, ranges, network, geometric, full text, domain, composite, and vector types, plus TOAST storage and collation control: see references/details.md .
Types to avoid
Avoid Use instead
timestamp (without time zone) timestamptz
char(n) , varchar(n) text (+ CHECK on length if needed)
money numeric
timetz timestamptz
timestamptz(0) or any precision timestamptz
serial generated always as identity
Constraints
PK : implicit UNIQUE + NOT NULL; creates a B tree index.
FK : specify ON DELETE/UPDATE ( CASCADE , RESTRICT , SET NULL , SET DEFAULT ). Index the referencing column. Use DEFERRABLE INITIALLY DEFERRED for circular dependencies checked at commit.
UNIQUE : creates a B tree index; allows multiple NULLs unless NULLS NOT DISTINCT (PG15+). Prefer NULLS NOT DISTINCT unless duplicate NULLs are wanted.
CHECK : row local; NULL passes (three valued logic). Combine with NOT NULL : price NUMERIC NOT NULL CHECK (price 0) .
EXCLUDE : prevents overlaps with operators, e.g. EXCLUDE USING gist (room id WITH =, booking period WITH &&) stops double booking. Needs a GiST capable type.
Indexing
B tree : default for equality/range ( = , < , , BETWEEN , ORDER BY ).
Composite : leftmost prefix rule ( WHERE a = ? AND b ? uses (a,b) ; WHERE b = ? does not). Most selective columns first.
Covering : CREATE INDEX ON tbl (id) INCLUDE (name, email) for index only scans.
Partial : hot subsets, CREATE INDEX ON tbl (user id) WHERE status = 'active' .
Expression : CREATE INDEX ON tbl (LOWER(email)) ; the query must use the same expression.
GIN : JSONB containment/existence, arrays, full text search. GiST : ranges, geometry, exclusion constraints.
BRIN : large, naturally ordered data (time series) at minimal storage cost; effective when disk order correlates with the indexed column.
Partitioning
Use for large tables ( 100M rows) whose queries consistently filter on the partition key, or where maintenance (pruning, bulk replacement) follows a key.
RANGE for time series ( PARTITION BY RANGE (created at) ; TimescaleDB automates it with retention and compression), LIST for discrete values, HASH for even distribution without a natural key.
Constraint exclusion : the planner prunes partitions through their CHECK constraints; declarative partitioning (PG10+) creates them for you.
Prefer declarative partitioning or hypertables. Do NOT use table inheritance.
Limitations : no global UNIQUE constraints—include the partition key in PK/UNIQUE. FKs from partitioned tables need PG11+, FKs referencing a partitioned table need PG12+; on older versions, use triggers.
Examples
Going deeper
references/details.md holds the material this file only names:
The full data type catalog: TOAST storage, collations, arrays, ranges, network, geometric, text search, domains, composites, vectors.
Table types ( TEMPORARY , UNLOGGED ) and row level security.
Constraint and index notes, and partitioning DDL for RANGE, LIST, and HASH.
Workload patterns: update heavy, insert heavy, upsert design, safe schema evolution.
Generated columns and extensions ( pg trgm , citext , timescaledb , postgis , pgvector , and more).
JSONB indexing strategies, including jsonb path ops and extracted B tree columns.