Database Cost Optimization: Instance Sizing, Reserved Instances, Storage Tiering
Database Cost Optimization: Instance Sizing, Reserved Instances, Storage Tiering
Database costs are often the largest infrastructure expense for data-intensive applications. This article covers strategies to optimize database spending without sacrificing performance or reliability.
Right-Sizing Instances
Over-provisioning is the most common cause of wasted database spend. Most teams provision for peak load and never scale down.
Measuring Utilization
-- CPU utilization (via pg_stat_statements)
SELECT ROUND(AVG(extract(epoch FROM total_exec_time) / calls), 2) AS avg_query_time
FROM pg_stat_statements;
-- Connection utilization
SELECT count(*) AS connections, setting AS max_connections
FROM pg_settings, pg_stat_activity
WHERE name = 'max_connections'
GROUP BY setting;
-- Disk IOPS
SELECT schemaname, tablename,
seq_tup_read, idx_tup_fetch,
seq_scan, idx_scan
FROM pg_stat_user_tables
ORDER BY seq_scan DESC;
In cloud environments, monitor CloudWatch (AWS), Azure Monitor, or GCP Cloud Monitoring metrics:
* **CPU Utilization**: If consistently below 20%, downsize.
* **Database Connections**: If below 30% of max, reduce max_connections or instance size.
* **Read/Write IOPS**: Provisioned IOPS that are never used are pure waste.
* **Storage Used**: Wasted storage costs money; reclaim unused space.
The Sizing Process
# Before: 8 vCPU, 32 GB RAM, 1000 GB gp2 ($700/month)
# After monitoring for 2 weeks:
# Peak CPU: 25%, average CPU: 12%
# Peak connections: 40 of 200
# Storage used: 120 GB
# Optimized: 4 vCPU, 16 GB RAM, 150 GB gp3 ($200/month - 71% savings)
Reserved Instances
Cloud providers offer significant discounts for committing to 1-year or 3-year terms:
| Commitment | AWS RDS Discount | Azure SQL Discount | GCP Cloud SQL | |------------|-----------------|-------------------|---------------| | 1-year no upfront | ~30% | ~30% | ~25% | | 1-year partial upfront | ~35% | ~35% | ~30% | | 3-year all upfront | ~60% | ~55% | ~50% |
When to Reserve
* **Reserve when**: Workload is predictable and runs 24/7. Production databases are ideal.
* **Do not reserve when**: Development/staging instances that run only during business hours. Short-lived project databases.
Using Reserved Instances with Auto-Scaling
If you use read replicas that scale dynamically, consider a mixed strategy:
Production primary: 3-year reserved instance (60% savings)
Read replicas (fixed): 1-year reserved (30% savings)
Read replicas (auto-scaling): On-demand (flexibility premium)
Storage Tiering
Different data needs different storage performance:
AWS RDS Storage Options
| Type | IOPS/GB | Max IOPS | Cost/GB/Month | Use Case | |------|---------|----------|---------------|----------| | gp2 | 3 baseline | 16,000 | $0.10 | Development, low-throughput workloads | | gp3 | 3,000 baseline | 16,000 | $0.08 | General purpose (cheaper than gp2) | | io1 | Provisioned | 256,000 | $0.125 + IOPS | High-throughput OLTP | | io2 | Provisioned | 256,000 | $0.125 + IOPS | Mission-critical, 99.999% durability |
Storage Tiering Strategy
-- Move historical data to cheaper storage
-- 1. Partition by date
CREATE TABLE orders (
id BIGSERIAL,
created_at TIMESTAMPTZ NOT NULL,
-- ...
) PARTITION BY RANGE (created_at);
CREATE TABLE orders_current PARTITION OF orders
FOR VALUES FROM ('2026-01-01') TO ('2027-01-01')
TABLESPACE fast_ssd;
CREATE TABLE orders_archive PARTITION OF orders
FOR VALUES FROM ('2020-01-01') TO ('2025-01-01')
TABLESPACE cold_hdd;
S3 Integration for Long-Term Storage
-- Using pg_export or custom archival to S3
-- Archive older partitions to S3
SELECT aws_s3.query_export_to_s3(
'SELECT * FROM orders WHERE created_at < ''2025-01-01''',
's3://myapp-backups/archived_orders/',
'orders_20250101.csv'
);
DROP TABLE orders_archive;
Serverless Databases
Serverless databases (Aurora Serverless, Cloud SQL, Azure Serverless) scale compute automatically and charge only for usage:
Aurora Serverless v2
# Automatic scaling from 0.5 to 64 ACUs
# 1 ACU = ~2 GB RAM, proportional CPU
aurora:
engine: aurora-postgresql
serverlessv2:
min_capacity: 0.5
max_capacity: 16
scaling_configuration:
seconds_until_auto_pause: 300
auto_pause: true
Cost scenarios:
| Workload | Traditional (db.r6g.large) | Serverless | |----------|---------------------------|------------| | 24/7 moderate | $170/month | $180/month (slightly more) | | Development (8 hours/day) | $170/month | $60/month (65% savings) | | Spiky/unpredictable | $340/month (over-provisioned) | $120/month (65% savings) |
Data Compression Savings
As discussed in the compression article, compression directly reduces storage costs:
-- Before: 200 GB table on gp3 ($16/month)
-- After page compression with zstd: 60 GB ($5/month)
-- Savings: $11/month per table
ALTER TABLE large_logs SET (compression = 'zstd');
Practical Cost-Saving Checklist
* [ ] Right-size instances based on 2-week utilization data.
* [ ] Purchase reserved instances for production databases.
* [ ] Use gp3 instead of gp2 (same performance, lower cost).
* [ ] Archive or delete unused data.
* [ ] Remove unused indexes (each index adds write overhead and storage cost).
* [ ] Enable compression on compressible data.
* [ ] Use read replicas instead of larger instances for read scaling.
* [ ] Consider serverless for development and variable workloads.
* [ ] Set storage autoscaling with limits to prevent runaway costs.
* [ ] Monitor and alert on cost anomalies.
Monitoring Cost Efficiency
-- Track wasted storage: dead tuples
SELECT schemaname, tablename,
n_dead_tup * 8192 AS wasted_bytes
FROM pg_stat_user_tables
ORDER BY wasted_bytes DESC;
Set up cloud cost budgets and alerts:
# AWS Budget example
aws budgets create-budget \
--account-id 123456789012 \
--budget file://budget.json
# Alert at 80% and 100% of monthly budget
Database cost optimization is an ongoing practice, not a one-time project. Review your database costs quarterly, right-size based on actual usage patterns, and leverage cloud provider discounts for predictable workloads.