Skip to content

Bits of .NET

Daily micro-tips for C#, SQL, performance, and scalable backend engineering.

  • Asp.Net Core
  • C#
  • SQL
  • JavaScript
  • CSS
  • About
  • ErcanOPAK.com
  • No Access
  • Privacy Policy
SQL

SQL Server: Find Slow Queries with Query Store (No Third-Party Tools Needed)

- 01.02.26 | 01.02.26 - ErcanOPAK

Application running slow but can’t figure out which queries are the culprits? Query Store is SQL Server’s built-in performance tracker that records everything.

Enable Query Store (SQL Server 2016+):

-- Enable for your database
ALTER DATABASE YourDatabaseName  
SET QUERY_STORE = ON (
    OPERATION_MODE = READ_WRITE,
    DATA_FLUSH_INTERVAL_SECONDS = 900,
    INTERVAL_LENGTH_MINUTES = 60,
    MAX_STORAGE_SIZE_MB = 1000,
    QUERY_CAPTURE_MODE = AUTO
);

-- Verify it's enabled
SELECT 
    name,
    is_query_store_on,
    query_store_retention_days
FROM sys.databases
WHERE name = 'YourDatabaseName';

Find Top 10 Slowest Queries:

SELECT TOP 10
    qsq.query_id,
    qsqt.query_sql_text,
    CAST(qsrs.avg_duration / 1000000.0 AS DECIMAL(10,2)) AS avg_duration_sec,
    qsrs.count_executions,
    CAST(qsrs.avg_cpu_time / 1000000.0 AS DECIMAL(10,2)) AS avg_cpu_sec,
    CAST(qsrs.avg_logical_io_reads AS BIGINT) AS avg_reads,
    qsrs.last_execution_time
FROM sys.query_store_query qsq
JOIN sys.query_store_query_text qsqt 
    ON qsq.query_text_id = qsqt.query_text_id
JOIN sys.query_store_plan qsp 
    ON qsq.query_id = qsp.query_id
JOIN sys.query_store_runtime_stats qsrs 
    ON qsp.plan_id = qsrs.plan_id
WHERE qsrs.last_execution_time >= DATEADD(hour, -24, GETUTCDATE())
ORDER BY qsrs.avg_duration DESC;

Real Output Example:

query_id | avg_duration_sec | count_executions | avg_cpu_sec | avg_reads
---------|------------------|------------------|-------------|----------
12845    | 45.23            | 1,200            | 38.5        | 1,200,000
  SQL: SELECT * FROM Orders o 
       JOIN OrderItems oi ON o.OrderId = oi.OrderId 
       WHERE o.CustomerId = @CustomerId  
  ↑ Missing index on CustomerId causing table scans

8392     | 12.67            | 5,800            | 10.2        | 450,000
  SQL: SELECT COUNT(*) FROM Products WHERE CategoryId IN (SELECT...)
  ↑ Subquery could be rewritten as JOIN

Find Queries with Degraded Performance:

-- Compare current vs historical performance
WITH QueryPerformance AS (
    SELECT 
        qsq.query_id,
        qsqt.query_sql_text,
        qsrs.runtime_stats_interval_id,
        CAST(qsrs.avg_duration / 1000000.0 AS DECIMAL(10,2)) AS avg_duration_sec,
        qsrsi.start_time,
        ROW_NUMBER() OVER (
            PARTITION BY qsq.query_id 
            ORDER BY qsrsi.start_time DESC
        ) AS rn
    FROM sys.query_store_query qsq
    JOIN sys.query_store_query_text qsqt ON qsq.query_text_id = qsqt.query_text_id
    JOIN sys.query_store_plan qsp ON qsq.query_id = qsp.query_id
    JOIN sys.query_store_runtime_stats qsrs ON qsp.plan_id = qsrs.plan_id
    JOIN sys.query_store_runtime_stats_interval qsrsi 
        ON qsrs.runtime_stats_interval_id = qsrsi.runtime_stats_interval_id
)
SELECT 
    current.query_id,
    current.query_sql_text,
    previous.avg_duration_sec AS previous_avg_sec,
    current.avg_duration_sec AS current_avg_sec,
    CAST(
        ((current.avg_duration_sec - previous.avg_duration_sec) / previous.avg_duration_sec) * 100 
        AS DECIMAL(10,2)
    ) AS degradation_percent
FROM QueryPerformance current
JOIN QueryPerformance previous 
    ON current.query_id = previous.query_id 
    AND previous.rn = 7  -- Compare to 7 intervals ago (7 hours if interval=1h)
WHERE current.rn = 1
    AND current.avg_duration_sec > previous.avg_duration_sec * 1.5  -- 50% slower
ORDER BY degradation_percent DESC;

Force a Specific Query Plan:

-- If a query had better performance with an older plan
EXEC sp_query_store_force_plan 
    @query_id = 12845, 
    @plan_id = 15234;  -- The plan_id from when it was fast

-- View forced plans
SELECT 
    qsq.query_id,
    qsp.plan_id,
    qsp.is_forced_plan,
    qsqt.query_sql_text
FROM sys.query_store_query qsq
JOIN sys.query_store_plan qsp ON qsq.query_id = qsp.query_id
JOIN sys.query_store_query_text qsqt ON qsq.query_text_id = qsqt.query_text_id
WHERE qsp.is_forced_plan = 1;

Monitor Query Store Health:

-- Check Query Store disk usage
SELECT 
    current_storage_size_mb,
    max_storage_size_mb,
    CAST(
        (current_storage_size_mb * 100.0 / max_storage_size_mb) 
        AS DECIMAL(5,2)
    ) AS storage_percent_used
FROM sys.database_query_store_options;

-- Clean up old data if needed
ALTER DATABASE YourDatabaseName 
SET QUERY_STORE CLEAR ALL;

Automatic Tuning (SQL Server 2017+):

-- Enable automatic plan correction
ALTER DATABASE YourDatabaseName
SET AUTOMATIC_TUNING (FORCE_LAST_GOOD_PLAN = ON);

-- SQL Server will automatically detect plan regressions
-- and revert to the last known good plan

Related posts:

SQL SELECT * — The Hidden Future Bug

SQL Indexing: Why Your Query Ignores Indexes and Scans 10 Million Rows Instead

SQL: Multi-tenant Security with Row-Level Security (RLS)

Post Views: 1

Post navigation

.NET Core: Fix Memory Leaks by Understanding IDisposable and Using Patterns
Fix Deadlocks by Understanding Lock Ordering and Using Proper Isolation Levels

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

March 2026
M T W T F S S
 1
2345678
9101112131415
16171819202122
23242526272829
3031  
« Feb    

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (938)
  • How to add default value for Entity Framework migrations for DateTime and Bool (836)
  • Get the First and Last Word from a String or Sentence in SQL (828)
  • How to select distinct rows in a datatable in C# (801)
  • How to make theater mode the default for Youtube (736)
  • Add Constraint to SQL Table to ensure email contains @ (575)
  • How to enable, disable and check if Service Broker is enabled on a database in SQL Server (554)
  • Average of all values in a column that are not zero in SQL (523)
  • How to use Map Mode for Vertical Scroll Mode in Visual Studio (477)
  • Find numbers with more than two decimal places in SQL (441)

Recent Posts

  • C#: Saving Memory with yield return (Lazy Streams)
  • C#: Why Records are Better Than Classes for Data DTOs
  • C#: Creating Strings Without Memory Pressure with String.Create
  • SQL: Protecting Sensitive Data with Dynamic Data Masking
  • SQL: Writing Readable Queries with Common Table Expressions (CTE)
  • .NET Core: Handling Errors Gracefully with Middleware
  • .NET Core: Mastering Service Lifetimes (A Visual Guide)
  • Git: Surgical Stashing – Don’t Save Everything!
  • Git: Writing Commits That Your Future Self Won’t Hate
  • Ajax: Improving Perceived Speed with Skeleton Screens

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (938)
  • How to add default value for Entity Framework migrations for DateTime and Bool (836)
  • Get the First and Last Word from a String or Sentence in SQL (828)
  • How to select distinct rows in a datatable in C# (801)
  • How to make theater mode the default for Youtube (736)

Recent Posts

  • C#: Saving Memory with yield return (Lazy Streams)
  • C#: Why Records are Better Than Classes for Data DTOs
  • C#: Creating Strings Without Memory Pressure with String.Create
  • SQL: Protecting Sensitive Data with Dynamic Data Masking
  • SQL: Writing Readable Queries with Common Table Expressions (CTE)

Social

  • ErcanOPAK.com
  • GoodReads
  • LetterBoxD
  • Linkedin
  • The Blog
  • Twitter
© 2026 Bits of .NET | Built with Xblog Plus free WordPress theme by wpthemespace.com