| name | database-workflows |
| description | Database workflows - schema design, migrations, query optimization. Use when designing schemas, reviewing migrations, optimizing queries, preventing N+1 problems, or working with ORMs like Prisma, Drizzle, and TypeORM. |
| version | 1.0.0 |
| author | Claude Code SDK |
| tags | database, schema, migrations, sql |
Database Workflows
Quick reference for database work with Claude Code - schema design, migrations, query optimization, and ORM patterns.
Quick Reference
| Task | Key Action |
|---|---|
| Schema design | Normalize to 3NF, add indexes for queries |
| Migration review | Check reversibility, data preservation |
| Query optimization | Explain analyze, check indexes |
| N+1 prevention | Eager load relations, use joins |
| Index selection | Composite for multi-column WHERE |
When to Use This Skill
- Designing new database schemas
- Reviewing migration files before running
- Optimizing slow queries
- Debugging N+1 query problems
- Adding or reviewing indexes
- Working with Prisma, Drizzle, or TypeORM
Schema Design Checklist
Before creating or modifying schemas:
- Tables have singular names (
usernotusers) - Primary keys are
id(auto-increment or UUID) - Foreign keys follow
{table}_idpattern - Timestamps include
created_at,updated_at - Nullable columns are intentional
- Indexes cover common query patterns
- No redundant data (normalized to 3NF minimum)
See SCHEMA-DESIGN.md for detailed patterns.
Migration Workflow
Before Creating Migrations
# Prisma
bunx prisma migrate dev --create-only --name descriptive_name
# Drizzle
bunx drizzle-kit generate:pg --name descriptive_name
# TypeORM
bunx typeorm migration:generate -n DescriptiveName
Migration Review Checklist
- Migration is reversible (has down/rollback)
- No data loss on rollback
- Large tables use batched operations
- Indexes created CONCURRENTLY (if supported)
- Foreign key constraints don't lock tables
- Default values for new NOT NULL columns
See MIGRATIONS.md for strategies.
Query Optimization Quick Guide
Identify Slow Queries
-- PostgreSQL: Find slow queries
SELECT query, calls, mean_time, total_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;
-- MySQL: Enable slow query log
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
Analyze Queries
-- PostgreSQL
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) SELECT ...;
-- MySQL
EXPLAIN ANALYZE SELECT ...;
Common Optimizations
| Problem | Solution |
|---|---|
| Full table scan | Add index on WHERE columns |
| Filesort | Add index matching ORDER BY |
| Using temporary | Optimize GROUP BY, add composite index |
| Seq Scan on large table | Add covering index |
See QUERIES.md for detailed optimization.
N+1 Query Prevention
Problem Pattern
// BAD: N+1 queries
const users = await db.user.findMany();
for (const user of users) {
const posts = await db.post.findMany({ where: { userId: user.id } });
}
Solution Pattern
// GOOD: Single query with relation
const users = await db.user.findMany({
include: { posts: true }
});
Detection
// Prisma: Enable query logging
const prisma = new PrismaClient({
log: ['query', 'info', 'warn', 'error'],
});
// Drizzle: Use query builder with joins
const result = await db
.select()
.from(users)
.leftJoin(posts, eq(users.id, posts.userId));
Index Quick Reference
When to Add Indexes
| Query Pattern | Index Type |
|---|---|
WHERE col = ? |
B-tree on col |
WHERE col1 = ? AND col2 = ? |
Composite (col1, col2) |
WHERE col LIKE 'prefix%' |
B-tree on col |
WHERE col @@ to_tsquery(?) |
GIN full-text |
ORDER BY col |
B-tree on col |
WHERE col IN (...) |
B-tree on col |
When NOT to Add Indexes
- Small tables (< 1000 rows)
- Columns with low cardinality
- Write-heavy tables with rare reads
- Columns rarely used in WHERE/ORDER BY
Index Commands
-- PostgreSQL: Create without locking
CREATE INDEX CONCURRENTLY idx_name ON table(column);
-- Check index usage
SELECT schemaname, tablename, indexname, idx_scan
FROM pg_stat_user_indexes
ORDER BY idx_scan ASC;
-- Find missing indexes
SELECT relname, seq_scan, idx_scan,
seq_scan - idx_scan AS difference
FROM pg_stat_user_tables
WHERE seq_scan > idx_scan
ORDER BY difference DESC;
ORM Patterns
Prisma
// Schema definition
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("users")
@@index([email])
}
// Efficient query with select
const users = await prisma.user.findMany({
select: { id: true, email: true },
where: { email: { contains: '@company.com' } },
take: 10,
});
Drizzle
// Schema definition
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: varchar('email', { length: 255 }).notNull().unique(),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
}, (table) => ({
emailIdx: index('email_idx').on(table.email),
}));
// Efficient query with joins
const result = await db
.select({ id: users.id, email: users.email })
.from(users)
.where(like(users.email, '%@company.com'))
.limit(10);
TypeORM
// Entity definition
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
@Index()
email: string;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
@OneToMany(() => Post, post => post.user)
posts: Post[];
}
// Efficient query with QueryBuilder
const users = await userRepository
.createQueryBuilder('user')
.select(['user.id', 'user.email'])
.where('user.email LIKE :email', { email: '%@company.com' })
.take(10)
.getMany();
Database-Specific Patterns
PostgreSQL
-- UPSERT
INSERT INTO users (email, name)
VALUES ('test@example.com', 'Test')
ON CONFLICT (email)
DO UPDATE SET name = EXCLUDED.name, updated_at = NOW();
-- Array columns
ALTER TABLE users ADD COLUMN tags TEXT[];
CREATE INDEX idx_users_tags ON users USING GIN(tags);
SELECT * FROM users WHERE 'admin' = ANY(tags);
-- JSON columns
ALTER TABLE users ADD COLUMN metadata JSONB DEFAULT '{}';
CREATE INDEX idx_users_metadata ON users USING GIN(metadata);
SELECT * FROM users WHERE metadata->>'role' = 'admin';
MySQL
-- UPSERT
INSERT INTO users (email, name)
VALUES ('test@example.com', 'Test')
ON DUPLICATE KEY UPDATE name = VALUES(name), updated_at = NOW();
-- Full-text search
ALTER TABLE posts ADD FULLTEXT INDEX ft_content (title, content);
SELECT * FROM posts WHERE MATCH(title, content) AGAINST('search term');
SQLite
-- UPSERT
INSERT INTO users (email, name)
VALUES ('test@example.com', 'Test')
ON CONFLICT(email)
DO UPDATE SET name = excluded.name, updated_at = datetime('now');
-- Enable foreign keys (per connection)
PRAGMA foreign_keys = ON;
-- WAL mode for better concurrency
PRAGMA journal_mode = WAL;
Workflow: Schema Review
Prerequisites
- Schema file or migration to review
- Understanding of query patterns
Steps
Check Normalization
- No repeated groups
- All columns depend on primary key
- No transitive dependencies
Validate Relationships
- Foreign keys defined correctly
- Cascade rules appropriate
- Junction tables for many-to-many
Review Indexes
- Indexes on foreign keys
- Indexes on commonly queried columns
- Composite indexes in correct order
Check Constraints
- NOT NULL where required
- UNIQUE where appropriate
- CHECK constraints for valid ranges
Validation
- No N+1 patterns in expected queries
- Indexes support all common queries
- Schema can evolve without data loss
Workflow: Query Optimization
Prerequisites
- Slow query identified
- Access to EXPLAIN ANALYZE
Steps
Analyze Query Plan
- Run EXPLAIN ANALYZE
- Identify sequential scans
- Check join strategies
Identify Issues
- Missing indexes
- Incorrect join order
- Unnecessary columns in SELECT
Apply Fixes
- Add appropriate indexes
- Rewrite query if needed
- Use query hints if necessary
Verify Improvement
- Re-run EXPLAIN ANALYZE
- Compare execution times
- Test under load
Validation
- Query uses indexes effectively
- Execution time acceptable
- No regression in related queries
Common Mistakes
| Mistake | Fix |
|---|---|
| No index on foreign key | Add index on FK columns |
| SELECT * in production | Select only needed columns |
| N+1 in loops | Use eager loading or joins |
| Missing timestamps | Add created_at, updated_at |
| Nullable by default | Explicitly define NOT NULL |
| No migration rollback | Always write down migration |
Reference Files
| File | Contents |
|---|---|
| SCHEMA-DESIGN.md | Schema patterns, normalization, relationships |
| MIGRATIONS.md | Migration strategies, rollback, versioning |
| QUERIES.md | Query optimization, N+1 prevention, performance |