| name | database-ops |
| description | Operate databases in production with zero-downtime migrations, connection pooling, backup verification, and performance monitoring. Outputs migration workflow, pooler config, monitoring queries, and runbooks. |
| argument-hint | ["database type","workload pattern","team size","RTO/RPO requirements"] |
| allowed-tools | Read, Write, Bash |
Database Operations
Running a database in production requires more than backups. Zero-downtime migrations, connection pool management, query performance monitoring, and incident runbooks are the operational practices that keep databases healthy as applications scale.
Zero-Downtime Migrations
"""
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);
"""
"""
UPDATE users SET full_name = first_name || ' ' || last_name
WHERE full_name IS NULL;
"""
"""
ALTER TABLE users ALTER COLUMN full_name SET NOT NULL;
ALTER TABLE users DROP COLUMN first_name;
ALTER TABLE users DROP COLUMN last_name;
"""
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column("users", sa.Column("full_name", sa.String(255), nullable=True))
def downgrade():
op.drop_column("users", "full_name")
Connection Pool Configuration (PgBouncer)
[databases]
production = host=postgres-primary port=5432 dbname=app
[pgbouncer]
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25
min_pool_size = 5
reserve_pool_size = 5
server_idle_timeout = 600
client_idle_timeout = 0
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
server_round_robin = 1
DATABASE_CONFIG = {
"pool_size": 10,
"max_overflow": 5,
"pool_timeout": 30,
"pool_recycle": 1800,
"pool_pre_ping": True,
}
Performance Monitoring Queries
SELECT pid, now() - pg_stat_activity.query_start AS duration,
query, state, wait_event_type, wait_event
FROM pg_stat_activity
WHERE state != 'idle'
AND now() - pg_stat_activity.query_start > INTERVAL '5 seconds'
ORDER BY duration DESC;
SELECT schemaname, tablename, indexname,
idx_scan, idx_tup_read, idx_tup_fetch,
pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
AND schemaname = 'public'
ORDER BY pg_relation_size(indexrelid) DESC;
SELECT tablename,
pg_size_pretty(pg_total_relation_size(tablename::regclass)) AS total,
pg_size_pretty(pg_relation_size(tablename::regclass)) AS data,
round(100 * (n_dead_tup::float / nullif(n_live_tup + n_dead_tup, 0)), 1) AS dead_pct
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;
state, () pg_stat_activity state ;
waiting.pid waiting_pid, waiting.query waiting_query,
blocking.pid blocking_pid, blocking.query blocking_query
pg_stat_activity waiting
pg_stat_activity blocking
blocking.pid (pg_blocking_pids(waiting.pid))
waiting.granted;
Backup Verification
#!/bin/bash
set -e
BACKUP_FILE=$1
TEST_DB="backup_verify_$(date +%Y%m%d%H%M%S)"
echo "Creating test database $TEST_DB..."
createdb $TEST_DB
echo "Restoring backup..."
pg_restore -d $TEST_DB -v $BACKUP_FILE
echo "Running verification queries..."
psql $TEST_DB -c "SELECT COUNT(*) FROM users;" | grep -E "[0-9]+" || exit 1
psql $TEST_DB -c "SELECT COUNT(*) FROM orders WHERE created_at > NOW() - INTERVAL '7 days';" | grep -E "[0-9]+" || exit 1
echo "Checking foreign key integrity..."
psql $TEST_DB -c "
SELECT COUNT(*) FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE u.id IS NULL;"
echo "Backup verified ✓"
dropdb $TEST_DB
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Fix |
|---|
| Breaking migration in one step | DROP COLUMN while old code runs → errors | Expand/contract pattern |
| Direct connection to Postgres | 100 app pods × 10 connections = 1000 | PgBouncer in transaction mode |
| Unverified backups | "We have backups" but restore never tested | Automated restore + verification daily |
| No connection pool timeout | Waiting forever for connection = request stuck | pool_timeout = 5-10 seconds |
| Long-running migrations in transaction | Locks table for minutes | Use ALTER TABLE ... CONCURRENTLY; batch updates |
10 Rules
- Every schema migration follows expand/contract — backward compatible changes only.
- PgBouncer (or equivalent) between application and Postgres — direct connections don't scale.
- Backup restore verification runs daily — an untested backup is not a backup.
- Monitor slow queries proactively — pg_stat_statements shows patterns before they become incidents.
- VACUUM and ANALYZE scheduled regularly — autovacuum alone is insufficient for high-write tables.
- Connection pool timeouts are set — never wait indefinitely for a database connection.
- Large table changes use
CONCURRENTLY — CREATE INDEX CONCURRENTLY, ALTER TABLE with care.
- Lock monitoring alerts — unexpected lock waits are a leading indicator of performance incidents.
- Replica lag is a metric — alert when lag exceeds 30 seconds.
- Database operations are documented in runbooks — DBAs don't exist at 3am; the runbook does.