--- name: design-postgres-tables description: Design PostgreSQL table schemas. Use when modeling new data, adding tables, or reviewing/improving existing schema design. --- # Design Postgres Tables Skill Use when: designing new tables, modeling relationships, reviewing schema, or planning migrations. ## Column Conventions - **Primary key**: `id UUID PRIMARY KEY DEFAULT gen_random_uuid()` (prefer UUID over serial for distributed systems; use BIGSERIAL if you need sortable IDs) - **Timestamps**: always include `created_at TIMESTAMPTZ DEFAULT NOW()` and `updated_at TIMESTAMPTZ DEFAULT NOW()` - **Soft delete**: `deleted_at TIMESTAMPTZ DEFAULT NULL` (NULL = active) - **Foreign keys**: name as `_id`, e.g. `user_id`, `project_id` - Use `TEXT` over `VARCHAR(n)` unless you need DB-enforced length limits - Use `BOOLEAN` not `SMALLINT` for true/false - Use `NUMERIC(precision, scale)` for money, never `FLOAT` ## Naming Conventions - Tables: `snake_case`, plural: `users`, `project_sessions`, `api_tokens` - Columns: `snake_case`, singular - Indexes: `idx__`: `idx_users_email` - Foreign keys: `fk_
_`: `fk_orders_users` - Unique constraints: `uq_
_`: `uq_users_email` ## Standard Table Template ```sql CREATE TABLE items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Business columns name TEXT NOT NULL, description TEXT, status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'archived')), -- Relationships user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- Metadata metadata JSONB DEFAULT '{}', -- Audit created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ ); -- Indexes CREATE INDEX idx_items_user_id ON items(user_id); CREATE INDEX idx_items_status ON items(status) WHERE deleted_at IS NULL; CREATE INDEX idx_items_created_at ON items(created_at DESC); -- Auto-update updated_at CREATE TRIGGER items_updated_at BEFORE UPDATE ON items FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` ## Relationship Patterns - **One-to-many**: FK on the "many" side - **Many-to-many**: junction table with `_id` + `_id` + its own PK + timestamps - **Polymorphic**: `entity_type TEXT + entity_id UUID` (add check constraint for valid types) ## Index Strategy - Index all foreign keys - Index columns used in WHERE clauses with high selectivity - Partial indexes for filtered queries: `WHERE deleted_at IS NULL` - Composite indexes: put equality columns first, range columns last - JSONB: use GIN index for `@>` containment queries ## Design Checklist - [ ] Every table has UUID PK + created_at + updated_at - [ ] Foreign keys have explicit ON DELETE behavior (CASCADE vs RESTRICT vs SET NULL) - [ ] Enum-like columns use CHECK constraints or a lookup table - [ ] Money stored as NUMERIC not FLOAT - [ ] Indexes on all FK columns - [ ] No storing arrays as comma-separated strings (use ARRAY[] or junction table) - [ ] JSONB for truly dynamic/schemaless data only — not as a crutch