Azure SQL pricing can be confusing with multiple tiers, compute options, and pooling strategies. Here's how to optimise costs when running multiple databases.
Pricing Models Overview
| Model | Best For | Billing |
|---|---|---|
| Provisioned (DTU) | Predictable workloads | Fixed hourly rate |
| Provisioned (vCore) | More control, hybrid benefit | Fixed hourly rate |
| Serverless | Variable/dev workloads | Per-second compute + storage |
| Elastic Pool | Multiple similar databases | Shared resources, fixed rate |
Single Database Costs
For a General Purpose database with 4 vCores:
Provisioned:
- ~£0.52/hour = £380/month
- Always running, always billed
Serverless:
- ~£0.00012/vCore-second when active
- ~£5.38/100 vCore-hours used
- Auto-pause after inactivity (saves compute costs)
- Still pay for storage
When to Use Serverless
Serverless saves money when:
- Database is idle >50% of the time
- Workload is unpredictable
- Dev/test environments with occasional use
- Overnight batch jobs with long idle periods
resource "azurerm_mssql_database" "dev" {
name = "db-dev"
server_id = azurerm_mssql_server.this.id
sku_name = "GP_S_Gen5_2" # Serverless, Gen5, 2 vCores max
auto_pause_delay_in_minutes = 60 # Pause after 1 hour idle
min_capacity = 0.5 # Minimum vCores when active
storage_account_type = "Local" # Cheaper storage
max_size_gb = 32
}
Warning: First query after pause takes 1-2 minutes to resume.
Elastic Pools
For multiple databases with different peak times:
Database A: Peak 9am-12pm (uses 4 vCores)
Database B: Peak 2pm-5pm (uses 4 vCores)
Database C: Peak 6pm-9pm (uses 4 vCores)
Without pool: 3 x 4 vCores = 12 vCores provisioned
With pool: 4 vCores shared = £££ savings
Pool Sizing
resource "azurerm_mssql_elasticpool" "this" {
name = "pool-production"
resource_group_name = azurerm_resource_group.this.name
location = azurerm_resource_group.this.location
server_name = azurerm_mssql_server.this.name
sku {
name = "GP_Gen5"
tier = "GeneralPurpose"
family = "Gen5"
capacity = 4 # Total vCores for pool
}
per_database_settings {
min_capacity = 0 # Database can use 0 vCores when idle
max_capacity = 4 # Single database can burst to full pool
}
max_size_gb = 500 # Total storage for pool
}
resource "azurerm_mssql_database" "pooled" {
name = "db-in-pool"
server_id = azurerm_mssql_server.this.id
elastic_pool_id = azurerm_mssql_elasticpool.this.id
}
Pool Economics
Pool makes sense when:
- Sum of all database peaks > pool size
- Databases don't all peak simultaneously
- At least 3-4 databases in the pool
Example calculation:
- 5 databases, each needs up to 2 vCores at peak
- Peaks don't overlap significantly
- Individual: 5 x 2 = 10 vCores @ £0.26/hr = £1,898/month
- Pool: 4 vCores @ £0.52/hr + pool premium = ~£600/month
Hybrid Benefit
If you have SQL Server licenses with Software Assurance:
resource "azurerm_mssql_database" "this" {
name = "db-production"
server_id = azurerm_mssql_server.this.id
sku_name = "GP_Gen5_4"
license_type = "BasePrice" # Use existing license
}
Saves ~55% on compute costs.
Storage Optimization
Choose the Right Tier
- Local: Cheapest, locally redundant
- Zone: Zone redundant, +25% cost
- Geo: Cross-region backup, premium pricing
resource "azurerm_mssql_database" "this" {
storage_account_type = "Local" # For dev/test
# storage_account_type = "Zone" # For production
}
Right-Size Storage
You pay for provisioned storage, not used:
-- Check actual usage
SELECT
DB_NAME() as DatabaseName,
SUM(size * 8 / 1024) as AllocatedMB,
SUM(FILEPROPERTY(name, 'SpaceUsed') * 8 / 1024) as UsedMB
FROM sys.database_files
Reserved Capacity
For stable production workloads:
| Term | Discount |
|---|---|
| 1 year | ~33% |
| 3 year | ~55% |
Reservation applies to vCores, not specific databases. Move workloads around as needed.
Monitoring for Optimisation
// Find underutilized databases
AzureMetrics
| where ResourceProvider == "MICROSOFT.SQL"
| where MetricName == "cpu_percent"
| summarize AvgCPU = avg(Average), MaxCPU = max(Maximum) by Resource
| where AvgCPU < 10 and MaxCPU < 30
| project Resource, AvgCPU, MaxCPU
// Find serverless candidates (low usage, sporadic)
AzureMetrics
| where ResourceProvider == "MICROSOFT.SQL"
| where MetricName == "cpu_percent"
| summarize
AvgCPU = avg(Average),
HoursWithActivity = dcountif(bin(TimeGenerated, 1h), Average > 5)
by Resource, bin(TimeGenerated, 1d)
| where HoursWithActivity < 8 # Active less than 8 hours/day
Decision Matrix
Q: Is workload predictable?
├─ Yes: Provisioned or Elastic Pool
│ └─ Q: Have SQL licenses?
│ ├─ Yes: Use Hybrid Benefit
│ └─ No: Consider Reserved Capacity
└─ No: Serverless
└─ Q: Can tolerate cold start?
├─ Yes: Enable auto-pause
└─ No: Set min_capacity > 0
Need help optimising your Azure SQL costs? Get in touch - we help organisations reduce their cloud database spend.