| name | Database |
| description | Database patterns — migrations, indexes, N+1 prevention, query optimization. Triggers on database/migration/query/sql/orm keywords. |
Database Skill
Load this skill when working with databases, migrations, queries, or data modeling.
Rule
DATABASE CHANGES MUST BE SAFE AND REVERSIBLE!
- Always test migrations on staging
- Never delete data without backup
- Index queries, not tables
Migration Best Practices
Safe Migration Checklist
Dangerous Operations
| Operation | Risk | Safe Alternative |
|---|
| Drop column | Data loss | Add new column, migrate, then drop |
| Rename column | Breaks code | Add alias, dual-write, then rename |
| Add NOT NULL | Fails on existing nulls | Add nullable, backfill, then add constraint |
| Change type | Data corruption | Add new column, migrate data |
Migration Patterns
Adding Column Safely
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
UPDATE users SET phone = 'unknown' WHERE phone IS NULL LIMIT 1000;
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;
Zero-Downtime Column Rename
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);
UPDATE users SET full_name = name WHERE full_name IS NULL;
ALTER TABLE users DROP COLUMN name;
Index Strategies
When to Add Index
| Query Pattern | Index Type |
|---|
WHERE status = ? | Single column |
WHERE user_id = ? AND status = ? | Composite (user_id, status) |
WHERE email LIKE 'john%' | Single column (prefix only) |
ORDER BY created_at DESC | Single column DESC |
WHERE status = ? ORDER BY created_at | Composite (status, created_at) |
Index Rules
- Order matters in composite indexes
- Leftmost prefix rule
- Don't over-index - indexes slow writes
- Cover queries when possible
Composite Index Order
Check Index Usage
EXPLAIN ANALYZE SELECT * FROM users WHERE status = 'active';
EXPLAIN SELECT * FROM users WHERE status = 'active';
N+1 Prevention
The Problem
users = User.all()
for user in users:
print(user.posts.count())
Solutions
Eager Loading
users = User.objects.prefetch_related('posts').all()
$users = User::with('posts')->get();
const users = await prisma.user.findMany({
include: { posts: true }
});
Batch Loading
user_ids = [u.id for u in users]
posts = Post.where(user_id__in=user_ids).all()
posts_by_user = groupby(posts, 'user_id')
Detecting N+1
Transaction Patterns
Basic Transaction
with session.begin():
user = User(email='test@example.com')
session.add(user)
session.add(Profile(user=user))
await prisma.$transaction([
prisma.user.create({ data: userData }),
prisma.profile.create({ data: profileData }),
]);
await prisma.$transaction(async (tx) => {
const user = await tx.user.create({ data: userData });
await tx.profile.create({ data: { userId: user.id } });
});
Isolation Levels
| Level | Dirty Read | Non-Repeatable | Phantom |
|---|
| READ UNCOMMITTED | Yes | Yes | Yes |
| READ COMMITTED | No | Yes | Yes |
| REPEATABLE READ | No | No | Yes |
| SERIALIZABLE | No | No | No |
Default: READ COMMITTED (PostgreSQL), REPEATABLE READ (MySQL)
Connection Pooling
Why Pool
- Creating connections is expensive
- Databases have connection limits
- Pool reuses connections efficiently
Configuration
# Recommended settings
min_connections: 2
max_connections: 10
idle_timeout: 30s
connection_timeout: 5s
Pool Size Formula
connections = (core_count * 2) + effective_spindle_count
For SSD: connections = cores * 2 + 1
Framework Examples
datasource db {
url = env("DATABASE_URL")
connectionLimit = 10
}
const pool = new Pool({
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
});
Query Optimization
EXPLAIN Output
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM users WHERE email = 'test@example.com';
Common Optimizations
| Problem | Solution |
|---|
| Seq Scan on large table | Add index |
| Sort operation | Add index with ORDER BY columns |
| High buffer reads | Increase work_mem |
| Nested Loop with many rows | Consider JOIN strategy |
Query Tips
SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 100;
SELECT * FROM users WHERE EXISTS (
SELECT 1 FROM orders WHERE orders.user_id = users.id
);
SELECT id, name, email FROM users;
CREATE INDEX idx_users_status_email ON users(status) INCLUDE (email);
Backup Strategies
Backup Types
| Type | Speed | Size | Recovery |
|---|
| Full | Slow | Large | Fast |
| Incremental | Fast | Small | Medium |
| Differential | Medium | Medium | Medium |
Backup Schedule
Daily: Full backup
Hourly: Incremental backup
Realtime: WAL archiving (PostgreSQL) / Binlog (MySQL)
Test Restores
Schema Design Tips
Naming Conventions
users, order_items, user_profiles
created_at, updated_at, user_id
idx_users_email, idx_orders_user_id_status
fk_orders_user_id
Common Patterns
deleted_at TIMESTAMP NULL
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
id UUID DEFAULT gen_random_uuid() PRIMARY KEY
When to Use This Skill
- Writing database migrations
- Optimizing slow queries
- Designing database schema
- Fixing N+1 queries
- Setting up connection pooling
- Planning backup strategy