Database Schema Designer
Design production-ready database schemas with built-in best practices.
Covering SQL normalization, indexing strategy, migration management with
safe rollback patterns, query optimization, and multi-tenant architecture
patterns across PostgreSQL, MySQL, SQLite, MongoDB, and Vitess.
When to Use This Skill
Use this skill when:
- Designing a new database schema from scratch
- Reviewing an existing schema for performance or correctness
- Planning a database migration with safe rollback
- Optimizing slow queries on a production database
- Converting between database engines (MySQL → PostgreSQL, etc.)
- Designing multi-tenant data architectures
- Any request like "design a schema for X", "review my database",
"optimize this query", "create migrations", "normalize this table"
Safety Rules (Risk Tier L2)
Database operations can be destructive. This skill enforces:
- Never DROP without backup — Always generate backup commands first
- Always generate rollback — Every migration includes verified reversal
- Test migrations on staging — Never run directly on production
- Lock-aware design — Schema changes must consider lock duration
- Data integrity first — Validate before and after every migration
- No data loss — Backfill before dropping columns, migrate before deleting
Design Methodology
Phase 1: Domain Modeling
Start with entities, not tables. Map the domain before writing DDL:
DOMAIN CANVAS:
├── Entities: What things exist? (User, Order, Product, Invoice)
├── Relationships: How do they connect? (one-to-many, many-to-many)
├── Attributes: What properties do they have?
├── Constraints: What must always be true?
├── Access Patterns: What queries will run most often?
└── Growth Projections: How many rows? At what rate?
Phase 2: Schema Design — SQL
Normalization checklist:
Example: E-Commerce Schema
-- Users table (normalized, with soft delete)
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ -- soft delete
);
CREATE INDEX idx_users_email ON users(email) WHERE deleted_at IS NULL;
-- Products with inventory tracking
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sku TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
price_cents INTEGER NOT NULL CHECK (price_cents >= 0),
inventory_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_products_sku ON products(sku);
-- Orders with status state machine
CREATE TYPE order_status AS ENUM (
'pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled'
);
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
status order_status NOT NULL DEFAULT 'pending',
total_cents INTEGER NOT NULL DEFAULT 0,
shipping_address JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status)
WHERE status IN ('pending', 'processing');
-- Order items (many-to-many with quantities)
CREATE TABLE order_items (
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES products(id),
quantity INTEGER NOT NULL CHECK (quantity > 0),
unit_price_cents INTEGER NOT NULL,
PRIMARY KEY (order_id, product_id)
);
Phase 3: Indexing Strategy
The Index Selection Algorithm:
- WHERE clause columns → Candidate for index
- JOIN columns → Index foreign keys
- ORDER BY columns → Consider composite with WHERE cols
- SELECT columns → Consider covering indexes
- Cardinality check → Don't index low-cardinality columns alone
Index types by database:
| Database |
Index Types |
When to Use |
| PostgreSQL |
B-tree (default), Hash, GiST, GIN, BRIN, SP-GiST |
B-tree for equality/range; GIN for full-text/arrays; BRIN for large sequential data |
| MySQL/InnoDB |
B-tree (clustered PK), Full-text, Spatial |
B-tree for most cases; Full-text for text search |
| SQLite |
B-tree (default) |
All standard cases |
| MongoDB |
Single-field, Compound, Multikey, Text, Geospatial, Hashed, TTL |
Compound for common queries; TTL for expiring data |
Index anti-patterns:
- Indexing every column "just in case" — wastes write performance
- Missing composite index leading column — index on (a, b) doesn't help queries on b alone
- Redundant indexes — index on (a, b) makes index on (a) redundant
- Unused indexes — audit with
pg_stat_user_indexes or sys.dm_db_index_usage_stats
Phase 4: Migration Design
Migration file structure:
-- migrations/001_add_user_preferences.sql
-- UP: Forward migration (what to apply)
-- DOWN: Rollback migration (how to undo)
-- UP MIGRATION
BEGIN;
CREATE TABLE user_preferences (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
theme TEXT NOT NULL DEFAULT 'light',
notifications JSONB NOT NULL DEFAULT '{}',
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Backfill existing users with defaults
INSERT INTO user_preferences (user_id)
SELECT id FROM users
ON CONFLICT (user_id) DO NOTHING;
COMMIT;
-- DOWN MIGRATION
-- BEGIN;
-- DROP TABLE IF EXISTS user_preferences;
-- COMMIT;
Safe migration rules:
- Add columns → Safe (nullable with default)
- Drop columns → Three-step: stop writing → backfill → drop
- Rename columns → Two-step: add new + dual-write → drop old
- Change type → Add new column → backfill → switch reads → drop old
- Add NOT NULL → Add with default → backfill nulls → add constraint
- Create index → Use CONCURRENTLY (PostgreSQL) or ONLINE (MySQL 8+)
- Drop index → Use CONCURRENTLY if available
- Add foreign key → Validate existing data first, validate constraint
Phase 5: Query Optimization
The EXPLAIN-based optimization workflow:
- Run
EXPLAIN (ANALYZE, BUFFERS) on the slow query
- Identify the bottleneck: sequential scan? nested loop? sort?
- Check if statistics are up to date:
ANALYZE table_name
- Consider: new index, query rewrite, schema change, or config tuning
- Test and measure improvement (not just EXPLAIN cost, actual timing)
Common optimization patterns:
| Problem |
Symptom |
Fix |
| Missing index |
Seq Scan on large table |
Add covering index for WHERE + JOIN + SELECT |
| N+1 queries |
Many small queries |
Use JOIN or batch load |
| Over-fetching |
SELECT * on wide tables |
Select only needed columns |
| Lock contention |
UPDATE waiting on locks |
Batch updates, use SKIP LOCKED |
| Statistics stale |
Planner choosing wrong plan |
ANALYZE table; adjust default_statistics_target |
Phase 6: Multi-Tenant Architecture
| Pattern |
Description |
Best For |
Trade-offs |
| Database per tenant |
Separate DB per customer |
High security, compliance |
Many connections, harder cross-tenant queries |
| Schema per tenant |
Separate schema per customer |
Medium security, shared DB |
Easier backups, moderate isolation |
| Row-level (shared) |
tenant_id column everywhere |
Simplicity, shared resources |
Weakest isolation, query complexity |
| Hybrid |
Combine patterns by tier |
Enterprise customers get isolation |
Operational complexity |
NoSQL Design Patterns
MongoDB Schema Design
// Denormalized by access pattern
db.products.insertOne({
_id: ObjectId(),
name: "Widget",
price: 9.99,
// Embedded reviews (accessed together)
reviews: [
{ user: "alice", rating: 5, comment: "Great!" },
{ user: "bob", rating: 4, comment: "Good value" }
],
// Reference pattern for frequently-updated data
inventory_warehouse_id: ObjectId("...")
});
MongoDB data modeling rules:
- Embed for "contains" relationships (order → line items)
- Reference for "uses" relationships (order → product)
- Embed when data is read together, updated together
- Reference when data is shared across documents
- Size limit: documents must be under 16MB
Key-Value (Redis) Design
# Session store with TTL
SETEX session:abc123 3600 '{"user_id": 42, "role": "admin"}'
# Rate limiting with sliding window
INCR rate:user:42
EXPIRE rate:user:42 60
# Leaderboard with sorted sets
ZADD leaderboard:weekly 1000 player:42 950 player:17
ZREVRANGE leaderboard:weekly 0 9 WITHSCORES
Platform Notes
- All platforms: This skill provides declarative knowledge — the agent
applies patterns using its existing database tools and query capabilities.
- Risk tier L2: Schema design is read-heavy during design phases.
Migration execution requires human approval gates.
- Database-specific tools: The skill supports SQL command generation but
defers to the agent's MCP or native database tools for actual execution.
1---2name: database-schema-designer3description: Design production-ready database schemas for SQL and NoSQL databases. Covers normalization, indexing strategy, migration management with rollback safety, query optimization, and multi-tenant patterns. Supports PostgreSQL, MySQL, SQLite, MongoDB, and Vitess.4license: MIT5---67# Database Schema Designer89Design production-ready database schemas with built-in best practices.10Covering SQL normalization, indexing strategy, migration management with11safe rollback patterns, query optimization, and multi-tenant architecture12patterns across PostgreSQL, MySQL, SQLite, MongoDB, and Vitess.1314## When to Use This Skill1516Use this skill when:17- Designing a new database schema from scratch18- Reviewing an existing schema for performance or correctness19- Planning a database migration with safe rollback20- Optimizing slow queries on a production database21- Converting between database engines (MySQL → PostgreSQL, etc.)22- Designing multi-tenant data architectures23- Any request like "design a schema for X", "review my database",24 "optimize this query", "create migrations", "normalize this table"2526## Safety Rules (Risk Tier L2)2728Database operations can be destructive. This skill enforces:29301. **Never DROP without backup** — Always generate backup commands first312. **Always generate rollback** — Every migration includes verified reversal323. **Test migrations on staging** — Never run directly on production334. **Lock-aware design** — Schema changes must consider lock duration345. **Data integrity first** — Validate before and after every migration356. **No data loss** — Backfill before dropping columns, migrate before deleting3637## Design Methodology3839### Phase 1: Domain Modeling4041Start with entities, not tables. Map the domain before writing DDL:4243```44DOMAIN CANVAS:45├── Entities: What things exist? (User, Order, Product, Invoice)46├── Relationships: How do they connect? (one-to-many, many-to-many)47├── Attributes: What properties do they have?48├── Constraints: What must always be true?49├── Access Patterns: What queries will run most often?50└── Growth Projections: How many rows? At what rate?51```5253### Phase 2: Schema Design — SQL5455**Normalization checklist:**56- [ ] 1NF: Atomic columns, no repeating groups57- [ ] 2NF: No partial dependencies on composite keys58- [ ] 3NF: No transitive dependencies59- [ ] BCNF: Every determinant is a candidate key (when needed)60- [ ] Denormalize intentionally (document why, measure benefit)6162**Example: E-Commerce Schema**6364```sql65-- Users table (normalized, with soft delete)66CREATE TABLE users (67 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),68 email TEXT NOT NULL UNIQUE,69 name TEXT NOT NULL,70 created_at TIMESTAMPTZ NOT NULL DEFAULT now(),71 updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),72 deleted_at TIMESTAMPTZ -- soft delete73);74CREATE INDEX idx_users_email ON users(email) WHERE deleted_at IS NULL;7576-- Products with inventory tracking77CREATE TABLE products (78 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),79 sku TEXT NOT NULL UNIQUE,80 name TEXT NOT NULL,81 price_cents INTEGER NOT NULL CHECK (price_cents >= 0),82 inventory_count INTEGER NOT NULL DEFAULT 0,83 created_at TIMESTAMPTZ NOT NULL DEFAULT now()84);85CREATE INDEX idx_products_sku ON products(sku);8687-- Orders with status state machine88CREATE TYPE order_status AS ENUM (89 'pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled'90);9192CREATE TABLE orders (93 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),94 user_id UUID NOT NULL REFERENCES users(id),95 status order_status NOT NULL DEFAULT 'pending',96 total_cents INTEGER NOT NULL DEFAULT 0,97 shipping_address JSONB NOT NULL,98 created_at TIMESTAMPTZ NOT NULL DEFAULT now()99);100CREATE INDEX idx_orders_user_id ON orders(user_id);101CREATE INDEX idx_orders_status ON orders(status)102 WHERE status IN ('pending', 'processing');103104-- Order items (many-to-many with quantities)105CREATE TABLE order_items (106 order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,107 product_id UUID NOT NULL REFERENCES products(id),108 quantity INTEGER NOT NULL CHECK (quantity > 0),109 unit_price_cents INTEGER NOT NULL,110 PRIMARY KEY (order_id, product_id)111);112```113114### Phase 3: Indexing Strategy115116**The Index Selection Algorithm:**1171181. **WHERE clause columns** → Candidate for index1192. **JOIN columns** → Index foreign keys1203. **ORDER BY columns** → Consider composite with WHERE cols1214. **SELECT columns** → Consider covering indexes1225. **Cardinality check** → Don't index low-cardinality columns alone123124**Index types by database:**125126| Database | Index Types | When to Use |127|----------|-------------|-------------|128| PostgreSQL | B-tree (default), Hash, GiST, GIN, BRIN, SP-GiST | B-tree for equality/range; GIN for full-text/arrays; BRIN for large sequential data |129| MySQL/InnoDB | B-tree (clustered PK), Full-text, Spatial | B-tree for most cases; Full-text for text search |130| SQLite | B-tree (default) | All standard cases |131| MongoDB | Single-field, Compound, Multikey, Text, Geospatial, Hashed, TTL | Compound for common queries; TTL for expiring data |132133**Index anti-patterns:**134- Indexing every column "just in case" — wastes write performance135- Missing composite index leading column — index on (a, b) doesn't help queries on b alone136- Redundant indexes — index on (a, b) makes index on (a) redundant137- Unused indexes — audit with `pg_stat_user_indexes` or `sys.dm_db_index_usage_stats`138139### Phase 4: Migration Design140141**Migration file structure:**142```sql143-- migrations/001_add_user_preferences.sql144-- UP: Forward migration (what to apply)145-- DOWN: Rollback migration (how to undo)146147-- UP MIGRATION148BEGIN;149CREATE TABLE user_preferences (150 user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,151 theme TEXT NOT NULL DEFAULT 'light',152 notifications JSONB NOT NULL DEFAULT '{}',153 updated_at TIMESTAMPTZ NOT NULL DEFAULT now()154);155-- Backfill existing users with defaults156INSERT INTO user_preferences (user_id)157SELECT id FROM users158ON CONFLICT (user_id) DO NOTHING;159COMMIT;160161-- DOWN MIGRATION162-- BEGIN;163-- DROP TABLE IF EXISTS user_preferences;164-- COMMIT;165```166167**Safe migration rules:**1681691. **Add columns** → Safe (nullable with default)1702. **Drop columns** → Three-step: stop writing → backfill → drop1713. **Rename columns** → Two-step: add new + dual-write → drop old1724. **Change type** → Add new column → backfill → switch reads → drop old1735. **Add NOT NULL** → Add with default → backfill nulls → add constraint1746. **Create index** → Use CONCURRENTLY (PostgreSQL) or ONLINE (MySQL 8+)1757. **Drop index** → Use CONCURRENTLY if available1768. **Add foreign key** → Validate existing data first, validate constraint177178### Phase 5: Query Optimization179180**The EXPLAIN-based optimization workflow:**1811821. Run `EXPLAIN (ANALYZE, BUFFERS)` on the slow query1832. Identify the bottleneck: sequential scan? nested loop? sort?1843. Check if statistics are up to date: `ANALYZE table_name`1854. Consider: new index, query rewrite, schema change, or config tuning1865. Test and measure improvement (not just EXPLAIN cost, actual timing)187188**Common optimization patterns:**189190| Problem | Symptom | Fix |191|---------|---------|-----|192| Missing index | Seq Scan on large table | Add covering index for WHERE + JOIN + SELECT |193| N+1 queries | Many small queries | Use JOIN or batch load |194| Over-fetching | SELECT * on wide tables | Select only needed columns |195| Lock contention | UPDATE waiting on locks | Batch updates, use SKIP LOCKED |196| Statistics stale | Planner choosing wrong plan | ANALYZE table; adjust default_statistics_target |197198### Phase 6: Multi-Tenant Architecture199200| Pattern | Description | Best For | Trade-offs |201|---------|-------------|----------|------------|202| **Database per tenant** | Separate DB per customer | High security, compliance | Many connections, harder cross-tenant queries |203| **Schema per tenant** | Separate schema per customer | Medium security, shared DB | Easier backups, moderate isolation |204| **Row-level (shared)** | tenant_id column everywhere | Simplicity, shared resources | Weakest isolation, query complexity |205| **Hybrid** | Combine patterns by tier | Enterprise customers get isolation | Operational complexity |206207## NoSQL Design Patterns208209### MongoDB Schema Design210211```javascript212// Denormalized by access pattern213db.products.insertOne({214 _id: ObjectId(),215 name: "Widget",216 price: 9.99,217 // Embedded reviews (accessed together)218 reviews: [219 { user: "alice", rating: 5, comment: "Great!" },220 { user: "bob", rating: 4, comment: "Good value" }221 ],222 // Reference pattern for frequently-updated data223 inventory_warehouse_id: ObjectId("...")224});225```226227**MongoDB data modeling rules:**228- Embed for "contains" relationships (order → line items)229- Reference for "uses" relationships (order → product)230- Embed when data is read together, updated together231- Reference when data is shared across documents232- Size limit: documents must be under 16MB233234### Key-Value (Redis) Design235236```237# Session store with TTL238SETEX session:abc123 3600 '{"user_id": 42, "role": "admin"}'239240# Rate limiting with sliding window241INCR rate:user:42242EXPIRE rate:user:42 60243244# Leaderboard with sorted sets245ZADD leaderboard:weekly 1000 player:42 950 player:17246ZREVRANGE leaderboard:weekly 0 9 WITHSCORES247```248249## Platform Notes250251- **All platforms:** This skill provides declarative knowledge — the agent252 applies patterns using its existing database tools and query capabilities.253- **Risk tier L2:** Schema design is read-heavy during design phases.254 Migration execution requires human approval gates.255- **Database-specific tools:** The skill supports SQL command generation but256 defers to the agent's MCP or native database tools for actual execution.