Postgres Table Design and Safe Migrations

Production-ready Postgres DDL: right types, keys, constraints, indexes mapped to queries, and lock-safe migrations for live tables.

Development / Backendpostgresqlschema-design
by AgentVoltv1.0.0
Free to sign up · every skill included with AgentVolt Pro

Before & after

Same prompt, same agent — the only difference is the skill.

Prompt“Design a PostgreSQL schema for a user activity tracking system”
Beforegeneric AI agent
When designing a Postgres schema for activity tracking, you want to think about the volume of data you'll be storing and how you'll query it. You'll probably have a users table, then an activity events table that stores things like event type, timestamp, and any relevant metadata. 

Some things to consider: Do you need indexes on timestamp columns for date range queries? Should you store all the event metadata in a JSON column, or break it out into separate columns? Think about normalization—are you duplicating any data? Also consider constraints like foreign keys to ensure data integrity.

It might be worth thinking about archival too—if you're storing millions of events, you don't want to scan the entire activity table on every query. Does that help, or would you like me to dive deeper into any specific area?
Afterwith Postgres Table Design and Safe Migrations
POSTGRES SCHEMA: User Activity Tracking

CORE TABLES:

1. USERS (Master dimension)
   Column: id [UUID PRIMARY KEY]
   Column: email [VARCHAR(255) NOT NULL UNIQUE]
   Column: created_at [TIMESTAMPTZ NOT NULL DEFAULT now()]
   Constraint: UNIQUE(email)
   Index: idx_users_created_at (for monthly cohort analysis)

2. ACTIVITY_EVENTS (Fact table—write-heavy)
   Column: id [BIGSERIAL PRIMARY KEY]
   Column: user_id [UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE]
   Column: event_type [VARCHAR(50) NOT NULL] -- e.g., 'login', 'purchase', 'view'
   Column: event_time [TIMESTAMPTZ NOT NULL DEFAULT now()]
   Column: metadata [JSONB DEFAULT '{}'] -- flexible attributes
   Column: session_id [UUID] -- group events into sessions
   
   Constraints:
   • FOREIGN KEY(user_id) ensures referential integrity
   • ON DELETE CASCADE removes events if user deleted (adjust if audit required)
   
   Indexes (CRITICAL for query performance):
   • PRIMARY KEY: idx_events_id (default)
   • idx_events_user_id (filter by user)
   • idx_events_event_time (range queries on date)
   • idx_events_user_time (composite: SELECT * FROM activity WHERE user_id = X AND event_time > NOW() - '7 days')
   • idx_events_event_type (cardinality ~20, speeds up reports by event type)
   • idx_events_metadata_gin (GIN index on JSONB for flexible queries on metadata fields)
   
   Partitioning (If >10M rows/month):
   • Partition by RANGE(event_time) quarterly: activity_q1_2025, activity_q2_2025
   • Keeps index size manageable, speeds up archival (DROP PARTITION vs DELETE)

3. EVENT_TYPES (Reference table—normalization)
   Column: id [SMALLINT PRIMARY KEY]
   Column: type_name [VARCHAR(50) UNIQUE NOT NULL]
   Column: description [TEXT]
   
   Why: VARCHAR(50) in activity_events is safe, but if cardinality is truly <100, consider foreign key to event_types for space efficiency
   Trade-off: Adds join cost, saves ~10% storage

QUERY PATTERNS OPTIMIZED:

Pattern 1: "Get all events for a user in past 7 days"
   SELECT * FROM activity_events 
   WHERE user_id = $1 AND event_time > now() - '7 days'::interval
   • Uses: idx_events_user_time
   • Cost: ~10ms (no full table scan)

Pattern 2: "Count events by type today"
   SELECT event_type, COUNT(*) FROM activity_events
   WHERE event_time >= DATE_TRUNC('day', now())
   GROUP BY event_type
   • Uses: idx_events_event_time, idx_events_event_type
   • Cost: ~100ms (depends on today's volume)

Pattern 3: "Find users with property X in metadata"
   SELECT DISTINCT user_id FROM activity_events
   WHERE metadata->>'campaign_id' = 'camp_123'
   • Uses: idx_events_metadata_gin
   • Cost: ~500ms (slower, JSONB queries less optimized than columns)

NORMALIZATION TRADE-OFFS:

Option A (Current—denormalized):
   • Pros: Fast writes, simple schema, flexible metadata
   • Cons: Some data duplication if user attributes change

Option B (Fully normalized):
   • Add columns to activity_events: user_segment, user_country (denormalize)
   • Pros: Faster analysis queries (no join needed)
   • Cons: Must update activity_events if user segment changes retroactively (hard)
   • Recommendation: Use Option A, denormalize ONLY for immutable user attributes at event time

ARCHIVAL STRATEGY:
   • After 12 months: Move activity_q1_2024 to archive schema (or cold storage)
   • Keep indexes only on last 12 months
   • Reduces active index footprint from 40GB to 10GB

TRAP TO AVOID:
   • Don't put user_email in activity_events (denormalization sin). If email changes, you now have stale data.
   • Don't rely on metadata alone—JSON queries are 5x slower than indexed columns.
   • Don't forget ON DELETE CASCADE—orphaned activity rows break reporting.

FINAL SCHEMA (DDL):
   CREATE TABLE users (
     id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
     email VARCHAR(255) NOT NULL UNIQUE,
     created_at TIMESTAMPTZ NOT NULL DEFAULT now()
   );

   CREATE TABLE activity_events (
     id BIGSERIAL PRIMARY KEY,
     user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
     event_type VARCHAR(50) NOT NULL,
     event_time TIMESTAMPTZ NOT NULL DEFAULT now(),
     metadata JSONB DEFAULT '{}',
     session_id UUID
   ) PARTITION BY RANGE(event_time);

   CREATE INDEX idx_events_user_time ON activity_events(user_id, event_time DESC);
   CREATE INDEX idx_events_event_type ON activity_events(event_type);
   CREATE INDEX idx_events_metadata_gin ON activity_events USING GIN(metadata);

Estimated footprint: 100M events/month = 15GB + indexes, 12-month retention.

About this skill

Your schema looked fine until the table had real data. Then a foreign key delete started scanning millions of rows, a unique constraint let duplicate NULLs through, or a one-line migration locked production for four minutes. This skill makes Claude design Postgres tables that enforce your rules in the database and change safely once traffic is live.

What it does

  • Picks Postgres-native types and keys with stated reasons: identity vs UUIDv7, timestamptz, numeric money, text plus CHECK instead of enum types that are hard to change.
  • Matches each invariant to the right constraint: CHECK, partial unique index, composite tenant foreign key, or exclusion constraint for no-overlap rules such as double bookings.
  • Sets a deliberate ON DELETE policy for every relationship and indexes the referencing side, which Postgres does not do for you.
  • Ties every index to the query it serves and drops redundant ones, using equality-then-range column ordering.
  • Writes lock-safe migrations for live tables: lock_timeout, CONCURRENTLY, NOT VALID then VALIDATE, batched backfills, and expand-then-contract renames.
  • Reviews pasted schemas and migrations, with findings ranked integrity first, then downtime risk, then performance, each paired with fix SQL.

What you get

Runnable DDL or migration SQL, a short list explaining each non-obvious decision, and an index-to-query map. When existing tables are affected, you also get step-by-step migration notes that name the lock each step takes.

Best for

Backend and full-stack developers building features on Postgres (including RDS, Supabase, and Neon), and teams that need to change tables already holding production data without downtime.

SKILL.md preview

SKILL.md
---
name: postgres-table-design-safe-migrations
description: Designs and reviews PostgreSQL tables as runnable DDL. It chooses types, keys, constraints, and ON DELETE policies, maps every index to a named query, and writes migrations that avoid long locks on live tables. Use when the user says "design the tables for this feature", "review my Postgres schema", "write a migration to add this column", "should this be a uuid or bigint", "my migration locked the table in production", "deletes got slow after we added the foreign key", "how do I stop double bookings at the database level", or "is jsonb fine here".
---

# Postgres Table Design and Safe Migrations

… (sign up to view the full skill)
Sign up to view, copy, and install the full skill

More development skills

View all Development skills →