Production DB — zero-downtime migrations, pooling, backup, retention.
Scanned 9/5/2026
Install to Claude Code
npx -y skills add Dev-Toolbelt/dev-team-agents --skill database-production --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Database Production?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/dev-toolbelt-database-production)More formats (shields.io, HTML) on the badges page.
---
name: database-production
description: Production DB — zero-downtime migrations, pooling, backup, retention.
---
## Zero-Downtime Migrations
Schema changes on live tables require backward-compatible sequencing. Never issue a blocking `ALTER TABLE` directly on a large production table.
### Safe change sequence
| Change | Safe approach |
|--------|--------------|
| Add a nullable column | `ALTER TABLE … ADD COLUMN` — non-blocking in Postgres 11+ |
| Add a NOT NULL column | Add as nullable → backfill → add `DEFAULT` → add `NOT NULL` constraint |
| Rename a column | Add new column → dual-write both → migrate reads → drop old column |
| Add an index | `CREATE INDEX CONCURRENTLY` — never without `CONCURRENTLY` in production |
| Drop a column | Remove app references first → deploy → then drop |
| Change a column type | Add new column → backfill → swap application code → drop old column |
### Lock-free pattern for large tables (shadow table)
```sql
-- 1. Create shadow table with new schema
CREATE TABLE orders_new (LIKE orders INCLUDING ALL);
ALTER TABLE orders_new ADD COLUMN status_v2 text;
-- 2. Backfill in batches (avoid full-table lock)
INSERT INTO orders_new SELECT *, status::text FROM orders WHERE id BETWEEN 1 AND 10000;
-- 3. Sync ongoing writes with a trigger during cutover window
-- 4. Rename atomically
BEGIN;
ALTER TABLE orders RENAME TO orders_old;
ALTER TABLE orders_new RENAME TO orders;
COMMIT;
```
### Migration versioning tools
| Tool | Ecosystem | File format |
|------|-----------|------------|
| **Flyway** | JVM, any | `V1.0__description.sql` |
| **Liquibase** | JVM, any | `changelog.xml` or `.sql` |
| **Alembic** | Python / SQLAlchemy | `versions/xxxx_description.py` |
| **golang-migrate** | Go, any | `000001_description.up.sql` |
| **Supabase migrations** | Supabase | `supabase/migrations/YYYYMMDDHHMMSS_description.sql` |
**Rules for all tools:**
- Forward-only by default. Rollback migrations only when they are tested and the rollback is faster than a hotfix.
- Migration files are immutable after they run in any non-dev environment.
- Run migrations in CI before deploying application code.
---
## Connection Pooling
Direct connections are expensive. Always place a pooler between the application and the database in production.
### PgBouncer modes
| Mode | How it works | Best for |
|------|-------------|---------|
| **Session** | Connection held for the client's lifetime | Apps that use session-level features (SET, advisory locks, prepared statements) |
| **Transaction** | Connection returned to pool after each transaction | Stateless APIs — the default for most web apps |
| **Statement** | Connection returned after each statement | Rare; requires all queries to be auto-commit |
**Key config parameters:**
```ini
max_client_conn = 1000 # total clients PgBouncer will accept
default_pool_size = 20 # connections per user/db pair to Postgres
reserve_pool_size = 5 # extra connections for burst
pool_mode = transaction
server_idle_timeout = 600
```
### Managed proxies
| Service | When to use |
|---------|------------|
| **AWS RDS Proxy** | RDS/Aurora — handles failover automatically; use when Lambda or ECS tasks create many short-lived connections |
| **Cloud SQL Auth Proxy** | GCP Cloud SQL — IAM-based auth, no password in connection string; run as a sidecar |
| **Azure AD auth + connection pooling** | Azure SQL — use built-in retry policies |
**Pool sizing formula:** `pool_size = (num_cores * 2) + effective_spindle_count`. For SSDs, `effective_spindle_count = 1`. Start conservative; grow with evidence.
---
## Replication and Read Replicas
### Physical vs. logical replication
| Type | What it copies | Use case |
|------|---------------|---------|
| **Physical (streaming)** | Entire cluster byte-by-byte | HA standby, failover |
| **Logical** | Selected tables/rows (change stream) | Cross-version upgrades, selective replication, CDC |
### Replica lag — what to monitor
```sql
-- On the primary (Postgres)
SELECT client_addr, state, sent_lsn, write_lsn, flush_lsn, replay_lsn,
(sent_lsn - replay_lsn) AS replication_lag_bytes
FROM pg_stat_replication;
-- On the replica
SELECT now() - pg_last_xact_replay_timestamp() AS replica_lag_seconds;
```
Alert threshold: lag > 30 s for OLTP replicas; tune per workload.
### Routing reads to replicas
- Route read-only queries (reports, exports, analytics) to replica by separate connection string.
- Never route writes to a replica — they will fail or silently buffer.
- Add retry logic for reads: if replica lag exceeds SLA, fall back to primary.
---
## Table Partitioning
Use partitioning when a table exceeds ~50 M rows or when you need fast partition-level operations (drops, archiving).
### Strategy selection
| Strategy | Partition key | Best for |
|----------|--------------|---------|
| **Range** | Date, timestamp, sequential ID | Time-series, logs, events |
| **Hash** | Any high-cardinality column | Distributing writes evenly (no clear range) |
| **List** | Low-cardinality categorical column | Region, status, tenant (small count) |
### PostgreSQL declarative partitioning
```sql
CREATE TABLE events (
id bigserial,
created_at timestamptz NOT NULL,
payload jsonb
) PARTITION BY RANGE (created_at);
CREATE TABLE events_2025_q1 PARTITION OF events
FOR VALUES FROM ('2025-01-01') TO ('2025-04-01');
```
**Index strategy:** indexes on partitioned tables must be created on the parent; Postgres replicates them to all partitions.
**Pruning:** `WHERE created_at BETWEEN …` allows Postgres to skip irrelevant partitions. Always filter on the partition key.
---
## Backup and Recovery
### Target metrics first
| Metric | Definition | Typical targets |
|--------|-----------|----------------|
| **RPO** (Recovery Point Objective) | Maximum acceptable data loss | 1 min for OLTP, 24 h for analytics |
| **RTO** (Recovery Time Objective) | Maximum acceptable downtime | 15 min for critical, 4 h for non-critical |
### PostgreSQL
- **WAL archiving** → enables point-in-time recovery (PITR). Configure `archive_command` to ship WAL segments to durable storage (S3, GCS).
- **Logical backups** (`pg_dump`) → portable, cross-version. Suitable for small-to-medium databases. Not a substitute for WAL archiving at scale.
- **Physical backups** (`pg_basebackup`, pgBackRest, Barman) → fast restore, PITR-capable. Use for large databases.
**Verify backups.** Restore to a staging environment on a schedule (weekly minimum). A backup you haven't tested is not a backup.
### Managed cloud backups
| Service | Backup | PITR |
|---------|--------|------|
| AWS RDS / Aurora | Automated daily snapshots + WAL | Yes, up to 35 days |
| GCP Cloud SQL | Automated snapshots | Yes |
| Supabase | Daily backups (Pro+), PITR on Enterprise | Yes (Enterprise) |
---
## Data Retention and Archiving
### Retention policy by data class
| Class | Example | Retention | Action |
|-------|---------|-----------|--------|
| Transactional | Orders, payments | 7 years (financial) | Archive to cold storage |
| User activity logs | Page views, events | 90 days | Delete or aggregate |
| Audit trail | Permission changes | 2–7 years | Archive, immutable |
| Temporary | Sessions, OTP codes | TTL (hours) | Delete on expiry |
### Archiving pattern
1. Move cold rows to an archive table or export to S3/GCS parquet.
2. Delete from the live table using batched deletes to avoid long locks.
3. If using partitioning, drop old partitions instead of `DELETE` — much faster.
```sql
-- Batched delete (avoids table-level lock)
DELETE FROM events WHERE created_at < now() - interval '90 days'
AND id IN (SELECT id FROM events WHERE created_at < now() - interval '90 days' LIMIT 1000);
```
### Compliance-driven deletion (GDPR / CCPA)
- Hard-delete PII on request; soft-deletes are not sufficient.
- Log the deletion event in an audit table (without the PII itself).
- Use encrypted columns or separate tables for PII to simplify deletion scope.
---
## Seed Data vs. Test Fixtures
| | Seed data | Test fixtures |
|--|-----------|--------------|
| **Purpose** | Initialize application state (reference tables, system users, config) | Provide known state for a specific test |
| **Scope** | Environment-wide | Test-scoped, isolated per test |
| **Idempotency** | Required — safe to run multiple times | Reset between tests |
| **Location** | `db/seeds/` or `seeds/` | `tests/fixtures/` or factory helpers |
| **In CI** | Run once after migrations | Reset per test suite or test |
**Seed script rules:**
- Use `INSERT … ON CONFLICT DO NOTHING` or `UPSERT` — never plain `INSERT` for seed data.
- Never depend on auto-increment IDs from seeds — use known UUIDs or slugs as stable references.
- Keep seeds minimal: only what the application cannot function without.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!