---
title: Stop Scaling Azure SQL: Find Real Performance Issues
description: Use Azure's built-in diagnostics to find real performance bottlenecks in your Azure SQL Database instead of scaling up immediately.
canonical: https://adamtheautomator.com/azure-sql-performance-tuning/
---

# Stop Scaling Azure SQL: Find Real Performance Issues

> Use Azure's built-in diagnostics to find real performance bottlenecks in your Azure SQL Database instead of scaling up immediately.

Source: https://adamtheautomator.com/azure-sql-performance-tuning/

---

- Stop Scaling Azure SQL: Find Real Performance Issues Tap to hide Home

- Tutorials

- Guidebooks

- Instructors

- Get Paid to Write

- Advertising

- Recommended Resources

- About Adam

                Search for:

-

-

-

-

# Stop Scaling Azure SQL: Find Real Performance Issues

        Published:3 September 2026 - 10 min. read

- Azure
- Azure SQL Database
- Databases
- DevOps

          [](https://adamtheautomator.com/author/adam-bertram/)

            [Adam Bertram](https://adamtheautomator.com/author/adam-bertram/)

            Read [more tutorials](https://adamtheautomator.com/author/adam-bertram/) by Adam Bertram!

-

-

        [](https://specopssoft.com/product/specops-password-auditor/?utm_source=adamtheautomator&utm_medium=referral&utm_campaign=adamtheautomator_referral_na&utm_content=display)
        Audit Active Directory for stale users, weak passwords, and other security risks with [Specops Password Auditor](https://specopssoft.com/product/specops-password-auditor/?utm_source=adamtheautomator&utm_medium=referral&utm_campaign=adamtheautomator_referral_na&utm_content=text).

Table of Contents

- What's Throttling Your Azure SQL Database
- Finding the Expensive Queries with Query Performance InsightReading the Top Queries Chart
- Why Plans Regress, and How Automatic Correction Fixes It
- Finding and Fixing Missing Indexes Without GuessingThe DMV Query That Surfaces Missing Indexes
- Letting Azure Tune Itself: Automatic Index Management and Its Safety RailsSafety Rails for Automatic Changes
- SQL Performance Tuning by Symptom: CPU, IO, Blocking, and Chatty ConnectionsQuery Anti-Patterns That Waste the Optimizer's Work
- Three Checks Ahead of a Resize
- Watching an Entire Estate with Database WatcherThe Four-Step Setup Flow
- Tune the Query Before You Tune the Invoice

                XFacebookLinkedIn
                Don't reach for a bigger compute tier the next time a query crawls. Raising the DTUs or adding vCores is the reflex, skipping the SQL performance tuning step that fixes the query itself. The invoice grows and the query is still slow.

Scaling up rarely fixes a slow Azure SQL Database. Most slow Azure SQL workloads are stalled on a stale execution plan, a missing index nobody built, or a query that hits the server five hundred times when it should hit it once. Scaling up hides that waste instead of fixing it. You keep paying for the cover-up every month afterward. Every query that costs the database meaningful work leaves a record in Query Store, and the default capture mode skips only what's infrequent or trivial. Which tool you open first decides how fast you find where the waste is hiding.

## What's Throttling Your Azure SQL Database

Azure SQL Database hides the machine from you on purpose: the platform owns the hardware, so there's no server to sign into and no trace flag to flip. [Traditional instance-level tuning](https://learn.microsoft.com/azure/azure-sql/database/performance-guidance?view=azuresql) a SQL Server DBA takes for granted simply isn't an option here. That gap shows up fastest for teams that [migrated a SQL Server database to Azure SQL](https://adamtheautomator.com/migrate-sql-server-azure-sql-database/), since the tuning muscle memory from the old server no longer applies.

Tuning moves up a layer, from server settings to query shape, index design, and the tools Microsoft built to replace those settings. Azure measures performance against one of two [purchasing models](https://learn.microsoft.com/azure/azure-sql/database/purchasing-models?view=azuresql): the DTU model, a blended score of CPU, memory, and I/O, or the vCore model, which scales compute and storage independently. When a database sits at its resource limit, every query on it slows down together.

Query Store is the foundation: every other tool here just reads what it captured. That single record feeds Query Performance Insight and Automatic Tuning, each drawing on it for a different kind of diagnosis.

## Finding the Expensive Queries with Query Performance Insight

[Query Performance Insight](https://learn.microsoft.com/azure/azure-sql/database/query-performance-insight-use?view=azuresql) is the fastest way into that telemetry: [Query Store](https://learn.microsoft.com/sql/relational-databases/performance/monitoring-performance-by-using-the-query-store) runs underneath it by default, capturing query text, execution plans, and runtime statistics on every new database. Confirm it's collecting with a two-line check run from whatever tool you use to [connect to Azure SQL Database](https://adamtheautomator.com/connect-to-azure-sql-database/):

SELECT actual_state_desc, desired_state_desc, current_storage_size_mb, max_storage_size_mb
FROM sys.database_query_store_options;

If actual_state_desc doesn't come back READ_WRITE, nothing downstream has data to work with. Query Performance Insight also needs a couple of hours of active capture before its charts render anything useful. Watch for one specific mismatch: desired_state_desc reading READ_WRITE while actual_state_desc reads READ_ONLY. That gap means Query Store filled its storage allocation and demoted itself, so current_storage_size_mb will already be sitting at max_storage_size_mb. Capture has silently stopped. Raise the storage cap or shorten the retention window before you trust any chart built on that data.

### Reading the Top Queries Chart

Once that's confirmed, Query Performance Insight (under Intelligent Performance in the portal, or the same telemetry through T-SQL) opens on the top five CPU consumers by default. The chart overlays a DTU percentage line against bars for the queries you select, and a Custom tab re-slices the same data by duration or execution count. Duration surfaces queries most likely to be locking resources and blocking other sessions. Execution count surfaces something easy to miss: ten milliseconds looks harmless until a query fires five thousand times an hour, racking up more cumulative cost than one slow query ever will. Microsoft's own guidance calls this ["chatty" query behavior](https://learn.microsoft.com/azure/azure-sql/database/query-performance-insight-use?view=azuresql#review-top-queries-per-execution-count).

Work the top three to five resource consumers first, then re-open the chart a day later and check whether the DTU line moved. Inefficient queries rarely distribute their damage evenly, so those few queries are usually the whole complaint.

## Why Plans Regress, and How Automatic Correction Fixes It

Query Performance Insight names the expensive queries; the execution plan the optimizer chose explains them. The optimizer compiles a plan based on the parameter values it sees on first run, then caches that plan for reuse, a shortcut called parameter sniffing. It works until a later execution passes very different parameter values, the cached plan turns out to be a poor fit, and the optimizer keeps using it anyway because nothing has told it to reconsider. The same mechanics govern [performance tuning in SQL Server](https://adamtheautomator.com/performance-tuning-in-sql-server/) running on-premises, where parameter sniffing was documented long before Azure SQL Database existed.

Most sudden CPU or I/O spikes with no obvious cause trace back to a plan that stopped matching the query it was built for. [Automatic Plan Correction](https://learn.microsoft.com/azure/azure-sql/database/automatic-tuning-overview?view=azuresql), the FORCE_LAST_GOOD_PLAN option, compares a regressed plan's performance against the history Query Store retained and forces the last plan that worked. Databases created after March 2020 ship with it enabled; older databases need it turned on explicitly:

ALTER DATABASE CURRENT
SET AUTOMATIC_TUNING (FORCE_LAST_GOOD_PLAN = ON);

Even with the option off, the engine keeps logging every regression into sys.dm_db_tuning_recommendations: the regressed and recommended plan IDs, the estimated CPU gain, and the exact sp_query_store_force_plan call to force it by hand. Query that view on the same schedule you check the DTU chart.

## Finding and Fixing Missing Indexes Without Guessing

Every disk-based nonclustered index pays off on reads and costs you on writes: more storage, and more work on every INSERT, UPDATE, and DELETE that touches the indexed columns. That tradeoff is why [index design](https://learn.microsoft.com/sql/relational-databases/sql-server-index-design-guide?view=sql-server-ver17) still matters even though Azure SQL Database can build indexes for you. The optimizer gets more cardinality information from a unique index than from a non-unique one on the same columns. Indexing a column with millions of rows but only a handful of distinct values (a status flag, a boolean) rarely earns back the write overhead it costs. Modifying an existing index with an INCLUDE column usually beats creating a near-duplicate one.

### The DMV Query That Surfaces Missing Indexes

During compilation, the optimizer logs any index it estimates would have significantly cut the cost of the query it just compiled, but only when that query earns a full optimization pass. Simple single-table lookups usually resolve with a trivial plan instead and skip that logging step entirely. Only one thing follows from a clean result here: nothing you've run yet was expensive enough to trigger full optimization. It says nothing about whether the workload actually has index gaps. That log lives in a set of dynamic management views, queryable from [SQL Server Management Studio](https://adamtheautomator.com/sql-server-management-studio/) or another T-SQL client. Joining them produces a ranked list of what's missing:

SELECT
    migs.avg_user_impact,
    mid.statement,
    mid.equality_columns,
    mid.inequality_columns,
    mid.included_columns
FROM sys.dm_db_missing_index_details AS mid
INNER JOIN sys.dm_db_missing_index_groups AS mig
    ON mid.index_handle = mig.index_handle
INNER JOIN sys.dm_db_missing_index_group_stats AS migs
    ON mig.index_group_handle = migs.group_handle
WHERE migs.avg_user_impact > 50
ORDER BY migs.avg_user_impact DESC;

(Tested on Azure SQL Database Basic tier, compatibility level 170, engine version 12.0.2000.8.)

avg_user_impact estimates the percentage cost reduction the index would deliver. Sort by that column first. Microsoft Learn's [Tune nonclustered indexes with missing index suggestions](https://learn.microsoft.com/sql/relational-databases/indexes/tune-nonclustered-missing-index-suggestions?view=sql-server-ver17) is explicit that these aren't mandates: "Missing index suggestions aren't prescriptions to create indexes exactly as suggested." The DMVs also reset on every restart. Creating every suggestion on a write-heavy table can trade one performance problem for a worse one. Stale statistics compound the same risk from the other direction: an outdated row-count estimate misleads the optimizer as badly as no index at all, so a new index judged against stale numbers gets blamed for the estimate's mistake, not its own.

Warning: A missing-index recommendation is a hypothesis, not a mandate. Check avg_user_impact against your actual write volume on that table before you build anything, and re-check it after the next restart clears the DMVs.

## Letting Azure Tune Itself: Automatic Index Management and Its Safety Rails

Automatic tuning runs the same missing-index analysis continuously across every database in an estate and acts on what it finds, adding two more advisor recommendations to FORCE_LAST_GOOD_PLAN: CREATE_INDEX, which builds a flagged index and verifies the workload got faster, and DROP_INDEX, which removes duplicates and anything [unused for 90 days or longer](https://learn.microsoft.com/azure/azure-sql/database/automatic-tuning-overview?view=azuresql).

ALTER DATABASE CURRENT
SET AUTOMATIC_TUNING (CREATE_INDEX = ON, DROP_INDEX = ON);

### Safety Rails for Automatic Changes

By default only FORCE_LAST_GOOD_PLAN is on; index creation and removal stay off until you opt in, a sensible default for a change that touches physical schema. Three safety rails govern what happens once you do opt in:

- The platform drops an index that doesn't measurably help and recreates it if the workload proves otherwise.

- If CPU, data I/O, or log I/O crossed 80% in the previous 30 minutes, the platform postpones the change rather than adding load to a database already under pressure.

- Validating whether an index helped takes 30 minutes to 72 hours, depending on how often the affected queries run.

Automatic tuning can't tell a critical customer-facing call from an overnight batch job, and optimizing for average latency is the wrong tradeoff when that call is bound by a hard SLA. A release pipeline running schema deployments can silently drop an automatically created index when its migration script doesn't know the index exists. Review recent tuning history without T-SQL through [Get-AzSqlDatabaseRecommendedAction](https://learn.microsoft.com/en-us/powershell/module/az.sql/get-azsqldatabaserecommendedaction) in PowerShell. The cmdlet takes a mandatory -AdvisorName alongside the resource group, server, and database. It returns nothing at all until the platform has enough workload history to make a recommendation. The advisors worth checking are CreateIndex, DropIndex, and ForceLastGoodPlan.

## SQL Performance Tuning by Symptom: CPU, IO, Blocking, and Chatty Connections

Not every symptom needs the same tool. Reaching for Query Performance Insight when the real problem is blocking wastes time during an incident. High CPU points toward [missing indexes and plan regressions](https://learn.microsoft.com/azure/azure-sql/database/high-cpu-diagnose-troubleshoot?view=azuresql). High I/O with normal CPU often means a scan where a seek should happen, visible in the execution plan itself. And [blocking or deadlocks](https://learn.microsoft.com/azure/azure-sql/database/understand-resolve-blocking?view=azuresql) show up as queries that look fine alone but pile up waiting on a lock another session holds, a symptom none of the other tools surface on their own, because the query causing the wait isn't the one that's slow.

Tool
Best for
Scope

Query Performance Insight
Top CPU, duration, or execution-count offenders, fast to open
Single or pooled database

Query Store
Plan history, regression detection, forcing plans
Single database

DMVs (sys.dm_db_missing_index_*, sys.dm_os_wait_stats)
Ad hoc, scriptable, point-in-time diagnosis
Single database, resets on restart

[Database watcher](https://learn.microsoft.com/azure/azure-sql/database-watcher-overview?view=azuresql)
Low-latency, estate-wide view
Entire Azure SQL estate

Azure SQL Insights, the estate-monitoring tool this workflow used before, was retired at the end of December 2024; database watcher is Microsoft's current recommendation for the same job, and it skips the collection-agent VM you'd otherwise have to provision and patch yourself.

### Query Anti-Patterns That Waste the Optimizer's Work

Four query shapes show up in almost every slow-query list:

- SELECT * against a wide table forces the engine to read columns the application never uses, and blocks the optimizer from using a narrow covering index even when one exists.

- N+1 patterns, one query per row instead of one query for the whole set, multiply round trips and network latency by however many rows the outer loop touches.

- Non-parameterized queries generate a fresh execution plan on every call instead of reusing a cached one, burning compile time a parameterized version would skip.

- Row-by-row inserts and updates instead of batched operations turn one possible round trip into hundreds.

[Connection pooling](https://learn.microsoft.com/azure/azure-sql/database/performance-guidance?view=azuresql#optimize-connectivity-and-connection-pooling) addresses a related but separate cost: opening a fresh physical connection for every request. Most data providers pool connections by default, but it's worth confirming. Opening a fresh connection per call adds that setup cost to every request, on top of the query itself.

## Three Checks Ahead of a Resize

Business Critical and Hyperscale exist because some workloads need more vCores, memory, or faster local storage than SQL performance tuning alone can deliver. Microsoft's [high-CPU troubleshooting guidance](https://learn.microsoft.com/azure/azure-sql/database/high-cpu-diagnose-troubleshoot?view=azuresql#when-to-add-cpu-resources) points the same direction: add CPU resources once your queries and indexes are already properly tuned, not before. Check the same three things every time:

- Whether CPU, memory, IO, or storage is consistently near its limit, not just spiking during one bad afternoon. The DTU line in Query Performance Insight over a week tells you more than a single alert does.

- Whether the top resource consumers from earlier are still running the same execution plan they were a month ago, or whether a regression crept in uncorrected.

- Whether the missing-index DMVs are still showing high-impact recommendations nobody's acted on.

Reality Check: a bigger compute tier bills the same waste at a higher rate. Scale only when the bottleneck survives query and index work.

Once all three come back clean, a higher service tier or a move from the DTU model to vCore is the right purchase.

## Watching an Entire Estate with Database Watcher

Checking thirty databases one at a time across Azure SQL Database and Managed Instance is a full-time job on its own (the on-premises boxes nobody's decommissioned stay a separate chore – database watcher doesn't reach them). [Database watcher](https://learn.microsoft.com/azure/azure-sql/database-watcher-overview?view=azuresql) is Microsoft's managed answer for that Azure-side estate view, and unlike its VM-based predecessor, nothing in the path runs on a VM you provision:

### The Four-Step Setup Flow

- Create a watcher resource and point it at SQL targets, the databases, elastic pools, or managed instances you want monitored, up to 100 per watcher.

- The watcher connects to each target directly and collects from more than 70 catalog views and DMVs. No collector agent, no VM to patch.

- Collected data lands in a central store you choose: an Azure Data Explorer cluster or a Real-Time Analytics database in Microsoft Fabric.

- Azure Workbooks-based dashboards sit on top: an estate dashboard with heatmaps across every monitored resource, plus a resource dashboard with the query, index, and wait-statistics detail Query Performance Insight gives one database at a time.

That four-step path collapses the ownership split Azure SQL Insights required. Azure runs the watcher and the dashboards; you only choose and size the data store that holds what they collect.

Pro Tip: Grant the watcher's identity only the server roles Microsoft documents for Azure SQL Database, membership in ##MS_ServerPerformanceStateReader##, ##MS_DefinitionReader##, and ##MS_DatabaseConnector##, nothing broader. Database watcher checks its own permissions on connect and disconnects outright if it holds anything extra, so an overly generous grant doesn't just violate least privilege. It stops data collection.

Once telemetry lands in the data store, [Kusto Query Language](https://learn.microsoft.com/azure/data-explorer/kusto/query/) (or T-SQL, since Azure Data Explorer answers both) queries across the entire monitored estate, and Azure Monitor alert templates notify you when a database's conditions need attention. Retention runs per database or per table, well past what a single database's Query Store was ever built to hold. Database watcher itself is still in preview: the watcher resource and its dashboards are free, but the Azure Data Explorer cluster or Fabric capacity backing the data store carries its own cost.

## Tune the Query Before You Tune the Invoice

None of this replaces judgment. Treat a [database advisor recommendation](https://learn.microsoft.com/azure/azure-sql/database/database-advisor-implement-performance-recommendations?view=azuresql) as the opening argument in SQL performance tuning; you still make the call. The invoice is the last thing you should be tuning.

  Hate ads? Want to support the writer? Get many of our tutorials packaged as an ATA Guidebook.

  [Explore ATA Guidebooks](https://adamtheautomator.com/ata-guidebooks/)

## More from ATA Learning and Partners

- ### Recommended Resources! Recommended Resources for Training, Information Security, Automation, and more!

- ### Get Paid to Write! ATA Learning is always seeking instructors of all experience levels. Regardless if you’re a junior admin or system architect, you have something to share. Why not write on a platform with an existing audience and share your knowledge with the world?

- ### ATA Learning Guidebooks ATA Learning is known for its high-quality written tutorials in the form of blog posts. Support ATA Learning with ATA Guidebook PDF eBooks available offline and with no ads!

## Categories

- IT Ops

- Cloud

- DevOps

- Home Ops

- Information Security

- Software Development

## Site

- Home

- Tutorials

- Guidebooks

- Instructors

- Get Paid to Write

- Advertising

- Recommended Resources

- About Adam

        Copyright 2026&copy; ATA Learning | [Privacy Policy](https://adamtheautomator.com/privacy/)

                                                        Don't be left behind with the ATA Learning Newsletter!

Looks like you're offline!
