postgresql-table-design/SKILL.md
Design and optimize a PostgreSQL-specific schema. Use for PostgreSQL best practices, data types, indexing, constraints, performance patterns, and advanced features.
npx skillsauth add bkircher/skills postgresql-table-designInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
text with check (length(field) <= N) over varchar(N).bigint generated always as identity; use uuid only for global uniqueness or when opacity is required.not null constraints where semantically needed, and use default values for common cases.timestamptz for event times, numeric for money, text for strings, bigint for integers, double precision for floats (or numeric for exact decimal math).snake_case for tables and columns.unique allows multiple NULLs. Use unique (...) nulls not distinct (PG 15+) to allow a single NULL.numeric(2,0) fails). No silent truncation/rounding.cluster is a one-off operation and isn't maintained after further inserts. Row order on disk follows insertion unless explicitly clustered.vacuum cleans them. Avoid wide-row and high-churn designs when possible.bigint generated always as identity; use uuid when federating/distributing or for opaque IDs. Use uuidv7(); use gen_random_uuid() for PostgreSQL 17 and earlier.bigint unless space matters, then use integer or smallint as appropriate.double precision. Use real only for critical storage constraints. Use numeric for decimals needing full precision.text. For limits, use check (length(col) <= n), not varchar(n); avoid char(n). Use bytea for binary. Large strings/binary (>2KB) are TOASTed. Default extended is optimal. Use collation/expression indexes on lower(col) or citext for case-insensitivity.numeric(p, s), not float.timestamptz for timestamps, date for dates, interval for durations. Avoid timestamp (without timezone). Use now() for transaction time, clock_timestamp() for wall time.boolean with not null if appropriate.create type ... as enum for stable sets. For evolving values, use text/int plus check or lookup tables.'{val1,val2}' or array[val1,val2].daterange, numrange, tstzrange for intervals; index with GiST.inet, cidr, macaddr for IP/network/MAC.point, line, polygon, circle; index with GiST. For advanced needs, use PostGIS.tsvector for docs, index with GIN. Specify language in to_tsvector()/to_tsquery().(col).field.vector from pgvector for embeddings/similarity search.timestamp (no timezone); use timestamptz.char(n) or varchar(n); use text.money type; use numeric.timetz; use timestamptz.timestamptz(0); use timestamptz.serial; use generated always as identity.temporary: Session-scoped, not logged, auto-dropped.unlogged: Fast, not crash-safe. Use for cache/staging.Enable with alter table tbl enable row level security, then set policies:
create policy user_access on orders for select to app_users using (user_id = current_user_id());
on delete/on update actions. Index referencing columns explicitly. For circular dependencies, use deferrable initially deferred.nulls not distinct, PG15+). Prefer nulls not distinct unless duplicate NULLs are needed.not null.include for index-only scans.status='active'.where must match expression.partition by range (created_at) and create partitions per period.
partition by list (region).partition by hash (user_id).fillfactor=90 to support HOT updates.copy or multi-row insert for bulk loads.bigint generated always as identity.on conflict (col1, col2) needs a matching unique index (not partial).excluded.column for referenced values and only update changed columns.do nothing is faster than do update if no changes required.create index concurrently for non-blocking index creation; not in transactions.not null with volatile defaults rewrites entire table. Non-volatile is fast.generated always as (...) stored for computed, indexable columns. Use virtual for columns that don't need to be stored.like/ilike.jsonb with GIN index:
jsonb_col @> '{"k":"v"}'jsonb_col ? 'k', any/all: ?|, ?&jsonb_path_ops GIN for heavy containment-only; disallows key existence ops.@> for JSONB arrays when needed.check(jsonb_typeof(config) = 'object') for safety.create table users (
user_id bigint generated always as identity primary key,
email text not null unique,
name text not null,
created_at timestamptz not null default now()
);
create unique index on users (lower(email));
create index on users (created_at);
create table orders (
order_id bigint generated always as identity primary key,
user_id bigint not null references users(user_id),
status text not null default 'PENDING' check (status in ('PENDING','PAID','CANCELED')),
total numeric(10,2) not null check (total > 0),
created_at timestamptz not null default now()
);
create index on orders (user_id);
create index on orders (created_at);
create table profiles (
user_id bigint primary key references users(user_id),
attrs jsonb not null default '{}',
theme text generated always as (attrs->>'theme') stored
);
create index profiles_attrs_gin on profiles using gin (attrs);
testing
Use when writing or updating unit tests (in any language).
devops
Use when writing Terraform.
tools
Automate browser interactions, test web pages and work with Playwright tests.
documentation
Use when asked to write a Jira ticket.