claudecodeui/claude-skills/design-postgres-tables.md

3.1 KiB

name description
design-postgres-tables 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 <table_singular>_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_<table>_<column(s)>: idx_users_email
  • Foreign keys: fk_<table>_<referenced_table>: fk_orders_users
  • Unique constraints: uq_<table>_<column(s)>: uq_users_email

Standard Table Template

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 <a>_id + <b>_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