Back to Blog
FinOps
4 min read

Azure SQL PaaS Pricing - Optimizing Multi-Database Costs

Azure SQLFinOpsCost OptimisationDatabasePricing

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

ModelBest ForBilling
Provisioned (DTU)Predictable workloadsFixed hourly rate
Provisioned (vCore)More control, hybrid benefitFixed hourly rate
ServerlessVariable/dev workloadsPer-second compute + storage
Elastic PoolMultiple similar databasesShared 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:

TermDiscount
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.

How mature is your cloud cost management?

Take our free 2-minute FinOps maturity test and get a personalised improvement roadmap.