Skip to main content
emnode
Cost

Right-size over-provisioned Azure SQL Database compute

One capability across single databases, elastic pools and Managed Instance: find every database whose sustained DTU or vCore utilisation sits well below the tier it is paying for, and scale the compute down to fit the real workload without breaking it.

14 min·10 sections·AZURE

Last reviewed

Right-sizing SQL compute: the basics

What does an over-provisioned Azure SQL Database actually look like?

An Azure SQL Database bills almost entirely on the compute tier you provision, not on the work it does. You pick a service objective, S3 in the DTU model, or GP_Gen5_8 in the vCore model, and you pay for that capacity every hour whether the database is flat out or idle. Over-provisioning shows up as a simple, stubborn pattern on the metrics: a database sitting on a large tier whose CPU and DTU utilisation rarely climbs above a fraction of what it is paying for. The two Azure Monitor metrics that expose it are 'cpu_percent' for vCore databases and 'dtu_consumption_percent' for DTU databases, and a database that peaks at fifteen or twenty per cent month after month is a database paying for compute it never uses.

Azure surfaces this in two places. Azure Monitor metrics give you the raw utilisation curve over time, and Azure SQL Database Advisor, together with Query Performance Insight, turns that curve into specific tuning recommendations: which queries dominate the consumption, and whether the workload genuinely fits a smaller compute size. To check whether a database fits a lower tier, Microsoft documents a simple test: divide the DTU number of the lower compute size by the DTU number of your current size, multiply by a hundred, and compare that headroom against your observed average and peak utilisation.

Most of this over-provisioning is drift, not a deliberate sizing decision. A database provisioned large for a one-off data load that never shrank back. An elastic pool set to a peak that a since-retired workload demanded. A Managed Instance sized for a migration cutover and never trimmed. The work is to find every database, pool and instance whose sustained utilisation sits well below its tier, confirm it has the headroom to drop a level, and scale it down, which on Azure SQL is an online operation with only a brief reconnect at the very end.

In this lesson you will learn how Azure SQL Database expresses compute utilisation, how to find every over-provisioned single database, elastic pool and Managed Instance in a subscription, and how to scale them down to fit the real workload without breaking the few with spiky demand. You will use Azure Monitor metrics and Azure SQL Database Advisor to read utilisation, and the az CLI and Bicep to right-size the tier.

Fun fact

The tier that outlived its workload

The most common cause of an over-provisioned Azure SQL Database is a one-off event that bumped the tier up and a missing step to bump it back down: a bulk data import, a migration cutover, a year-end batch run. Scaling Azure SQL up is a deliberate, well-remembered act under deadline pressure; scaling it back down is the quiet step nobody owns, so the database keeps charging the peak tier long after the workload that justified it has gone. Because the scale-down is online with only a brief reconnect at the end, the reason it does not happen is almost never technical. It is that nobody is looking at the utilisation curve.

Finding over-provisioned databases across a subscription

Priya is the platform lead at a SaaS company doing its first serious Azure cost review. The SQL line has grown quietly for two years and nobody is sure how much of it is real workload versus tiers that were bumped up and never trimmed.

Rather than guess, Priya starts by reading the actual utilisation. She lists the databases and their tiers, then pulls the CPU or DTU consumption over the last month so the genuinely busy databases can be separated from the ones renting headroom they never touch.

Pull average and peak compute utilisation for a database over the last 30 days. Low sustained numbers on a large tier mean over-provisioned compute.

$ az monitor metrics list --resource "$DBID" --metric cpu_percent --aggregation Average Maximum --interval PT12H --start-time 2026-05-18T00:00:00Z --output table
Timestamp Name Average Maximum
------------------- ------------- --------- ---------
2026-05-18 00:00 CPU percentage 11.4 18.9
2026-05-18 12:00 CPU percentage 9.7 16.2
2026-05-19 00:00 CPU percentage 12.1 21.5
# Peaks never reach 25%. This GP_Gen5_8 fits comfortably in GP_Gen5_4 with headroom to spare.

A database whose peak utilisation stays well under the headroom of the next tier down is a safe, immediate scale-down. For DTU databases, swap cpu_percent for dtu_consumption_percent.

How you decide a database is over-provisioneddeep dive

Over-provisioning resolves to one comparison: sustained utilisation against provisioned capacity. The two metrics that carry it are 'cpu_percent' for vCore databases and 'dtu_consumption_percent' for DTU databases. The DTU consumption metric is itself a roll-up: at any instant it equals the highest of CPU percentage, Data IO percentage and Log IO percentage, so a database can be CPU-idle yet DTU-bound on IO. Reading average and peak together over a representative window, ideally a full month that includes any month-end or weekly batch, tells you whether the headroom you are paying for is ever actually used.

To size the drop, Microsoft documents a direct test: divide the DTU (or vCore) number of the candidate lower tier by the number of your current tier and multiply by a hundred to get the proportion of today's capacity the smaller tier represents. If your observed peak utilisation sits comfortably below that proportion, the workload fits the smaller tier with headroom to spare. Azure SQL Database Advisor and Query Performance Insight add the qualitative half: which queries dominate the consumption, so you can tell a database that is genuinely busy from one whose few heavy queries could be tuned rather than out-sized.

The scale operation itself is online. Azure SQL keeps the database available while it provisions the new compute and copies state in the background, with only a brief drop of active connections at the very end of the switchover, which a connection retry handles cleanly. That is why right-sizing down is low risk: there is no maintenance window, no data migration, and if a workload turns out to need the capacity back, scaling up again is the same online operation in reverse.

What is the impact of leaving SQL compute over-provisioned?

The direct impact is recurring waste. An over-provisioned database charges the full tier every hour for capacity the workload never reaches, and because the charge is per tier rather than per usage, that waste does not show up as a spike; it sits flat on the bill month after month, which is exactly why it goes unnoticed. A single database two tiers too large, multiplied across an estate of dozens, is a material annual sum spent on headroom that buys no performance.

The second-order impact is that the waste compounds the wrong commitments. Reserved capacity and three-year commits are sized to current consumption, so committing while databases are over-provisioned locks the waste in for years at a discount that was never needed. Right-sizing first, then reserving the genuine baseline, is the order that captures both savings; reserving first cements the over-provisioning.

On the governance side, an estate where tiers are sized by the highest peak anyone remembers, rather than by measured utilisation, has no defensible answer to 'why is the SQL bill this size?' A right-sized estate, where every tier maps to an observed utilisation curve with documented headroom, is the artefact that turns a vague cost review into a closed question, and it frees budget that was being spent on nothing.

How do you right-size SQL compute safely?

Work the capability as one loop rather than scaling databases at random. The order matters: read the real utilisation before you touch a tier, so you never starve a workload that genuinely needs its headroom.

1. Inventory every database, pool and instance by utilisation

List every single database, elastic pool and Managed Instance with its current tier, then pull cpu_percent (vCore) or dtu_consumption_percent (DTU) average and peak over a representative window of at least a month that includes any batch or month-end peak. Treat this utilisation curve as the source of truth, not the tier somebody set during a migration.

2. Separate genuine load from rented headroom

For each resource, decide whether the headroom is ever used. Use the documented headroom test, lower-tier capacity divided by current-tier capacity, and check your observed peak sits below it. Query Performance Insight tells you whether a busy database is genuinely loaded or driven by a few heavy queries that could be tuned instead of out-sized.

3. Scale down, biggest safe saving first

Right-size the databases with the most wasted headroom and the most stable workloads first. Each scale-down is a single online command per database with only a brief reconnect at the end, so prioritise by the saving and the safety of the headroom, not by ease. Leave the genuinely busy and the spiky-seasonal ones for a short observation window.

4. Lock in the floor with reservations and serverless

Once tiers are right, cover the steady baseline with a one or three year reserved-capacity commit, which Microsoft documents at up to roughly a third off pay-as-you-go, so the discount applies to real consumption rather than over-provisioning. Move intermittent dev and test databases to serverless with auto-pause so they charge only for storage while idle.

# 1. Read 30-day average and peak compute for a vCore database before touching the tier.
DBID=$(az sql db show -g rg-data -s sql-prod -n appdb --query id -o tsv)
az monitor metrics list --resource "$DBID" \
  --metric cpu_percent \
  --aggregation Average Maximum \
  --interval P1D \
  --start-time 2026-05-18T00:00:00Z \
  --output table

# 2. Peaks stay well under the next tier down's headroom: scale GP_Gen5_8 -> GP_Gen5_4.
# This is an online operation; expect only a brief reconnect at the end.
az sql db update -g rg-data -s sql-prod -n appdb \
  --edition GeneralPurpose --family Gen5 --capacity 4

# DTU database equivalent: drop S6 to S3 (read dtu_consumption_percent first).
# az sql db update -g rg-data -s sql-prod -n appdb --edition Standard --service-objective S3

echo "appdb: scaled to GP_Gen5_4; saving recurs from the next billing hour"

Quick quiz

Question 1 of 5

A vCore Azure SQL Database sits on GP_Gen5_8 and its cpu_percent averages 11% and peaks at 21% over a full month. What does this most likely indicate?

You can now treat Azure SQL compute as one capability rather than a fixed line on the bill: inventory every database, pool and instance by its real utilisation, separate genuine load from rented headroom, scale the over-provisioned ones down with a single online command, and only then size reserved capacity over the right-sized baseline. The saving lands from the next billing hour and recurs every month after.

Back to the library