Turn Your Vehicle Into a Smart Earning Asset

While you’re not driving your car or bike, it can still be working for you. MOTOSHARE helps you earn passive income by connecting your vehicle with trusted renters in your city.

🚗 You set the rental price
🔐 Secure bookings with verified renters
📍 Track your vehicle with GPS integration
💰 Start earning within 48 hours

Join as a Partner Today

It’s simple, safe, and rewarding. Your vehicle. Your rules. Your earnings.

Database Performance Optimization in .NET + MSSQL


A Complete End-to-End Guide for High-Performance Data Access in ASP.NET Applications


PREFACE

Modern ASP.NET applications rely heavily on databases—not just to store records, but to power product features, analytics, transactions, recommendation systems, dashboards, and business-critical workflows. As your user base grows, the database often becomes the #1 bottleneck long before CPU, memory, or network capacity becomes an issue.

This book is designed to be the definitive, practical, full-stack guide to optimizing MSSQL performance for .NET developers. Unlike traditional DBA books or high-level architecture guides, this book focuses on:

  • How .NET interacts with MSSQL
  • How EF Core generates SQL
  • How to diagnose & tune queries
  • How to eliminate latency
  • How to reduce round-trips
  • How to scale reads, writes, concurrency, and throughput
  • How to use modern tools: DMVs, Profiler, Query Store
  • How to leverage AI for database tuning
  • How to write LINQ and SQL that perform under load
  • How to design replicas, stored procedures, indexes, and plans that scale

This is a battle-tested guide, refined through real-world experience optimizing production applications handling:

  • millions of daily requests
  • billions of records
  • high concurrency systems
  • real-time workloads

Whether you’re building a SaaS app, API, microservice, monolith, dashboard, reporting engine, or enterprise platform — the principles in this book will give you a 10X improvement in performance, and the confidence to tune any MSSQL-backed .NET application at expert level.

#ToolCategory / LayerWhy This (vs others)Typical Use-Cases
1Query Store (SQL Server)Built-in DB – Slow Query & Plan HistorySingle best built-in tool for tracking slow queries over time, seeing plan changes/regressions, and comparing before/after tuning. Replaces the need for ad-hoc logging of all queries.Identify top CPU/IO queries, find regressed queries after deployments, force a stable plan, measure impact of index/query changes.
2Execution Plans (Actual) (SSMS / ADS)Built-in DB – Per Query Deep DiveThe primary tool for understanding why a query is slow – shows scans vs seeks, key lookups, join types, spills, row estimates. No 3rd-party tool can replace this; others just visualize it differently.Tune individual queries, fix missing indexes, understand JOIN strategy, verify impact of SQL or index changes.
3DMVs (Dynamic Management Views) + Script LibraryBuilt-in DB – System-Wide DiagnosticsBest way to pull system-level insight: top CPU queries, missing indexes, index usage, spills, etc. More flexible than GUI tools; forms the backbone for advanced tuning.Run periodic health checks, detect unused/inefficient indexes, identify high-CPU or high-IO statements, spot TempDB and memory issues.
4Extended Events (modern Profiler)Built-in DB – Lightweight Runtime TracingPreferable to old Profiler for production-safe tracing. Captures real-time events with less overhead. Avoids duplication with Profiler; if you choose one, choose Extended Events.Capture long-running queries, deadlocks, blocking, parameter values, and performance issues in production with minimal overhead.
5Database Engine Tuning Advisor (DTA)Built-in DB – Index Recommendation EngineActs as an automated advisor on top of Query Store / traces. Not a replacement for your judgment, but a useful second opinion to suggest indexes/indexed views based on a workload.Quickly bootstrap indexing strategy for a new or legacy database, evaluate potential index impact before applying.
6Azure Data Studio (ADS) (with extensions)SQL IDE – Cross-Platform + Plan/Query WorkbenchLightweight, cross-platform SQL IDE with good execution plan view and extension ecosystem (Copilot integration, notebooks). Keeps you from needing multiple other SQL editors.Day-to-day query analysis, reading plans, running DMV scripts, documenting investigations in notebooks, using AI assistance (where available).
7Redgate SQL Monitor (or similar single full-stack SQL monitoring tool)3rd-Party – Continuous SQL Monitoring & AlertingInstead of many overlapping GUI tools, pick one mature monitoring suite. SQL Monitor is widely used: performance dashboards, alerts, blocking, waits, top queries. Avoids having separate “performance dashboard” tools.24/7 visibility into SQL Server, alerting on slow queries, deadlocks, long waits, CPU/IO spikes; historical performance timelines.
8Azure Application Insights (for .NET)APM – App ↔ DB LinkBest integrated way (for .NET) to see which HTTP request caused which SQL query, with timings and dependency traces. Avoids needing multiple APMs – pick one and this is the most natural if you’re in Azure/.NET world.Correlate slow API endpoints to specific slow SQL, understand full request path, track dependency failures and timeouts.
9k6 (or your preferred single load tool)Load Testing – External Workload GeneratorInstead of multiple load tools (JMeter/Locust/etc.), pick one. k6 is scriptable, modern, easy for devs. It avoids feature duplication with other load tools.Generate realistic load against ASP.NET APIs, validate DB performance under stress, run “before vs after” tuning experiments.
10BenchmarkDotNetCode-Level Micro-Benchmarking (.NET)Best-in-class for micro-benchmarks; no need for other micro-benchmark libs. Measures EF vs Dapper vs raw ADO, LINQ versions, allocation differences.Benchmark individual data-access paths, compare EF Core query shapes, verify optimization impact on small hot code paths.
11EF Core Logging & Diagnostic EventsORM-Level Visibility (No Extra Tool)Built-in, zero extra deployment, and gives precise visibility into what SQL EF generates and how often. Avoids needing separate EF-specific profilers.Detect N+1 queries, see generated SQL, measure query counts per request, understand loading behavior (lazy vs eager).
12Redis (Distributed Cache) + IMemoryCacheOffload DB Reads (Caching)Rather than multiple caching systems, pick MemoryCache (L1) + Redis (L2) as your standard. Directly reduces DB load & latency, so belongs in a performance toolkit.Cache lookups, reference data, frequently-read aggregates, reduce read pressure on MSSQL, speed up APIs by orders of magnitude.
13GitHub Copilot / AI + ChatGPT (or similar AI assistant)AI-Assisted OptimizationNot a replacement for Query Store/Plans, but immensely useful for explaining plans, rewriting SQL, suggesting indexes, improving LINQ. Avoid multiple AI tools: pick 1–2 you trust.Paste queries/plans, ask for optimization suggestions, generate index ideas, convert problematic LINQ to efficient SQL-shaped queries.

🏛️ PART I — FOUNDATIONS OF DATABASE PERFORMANCE


CHAPTER 1 — Understanding Database Performance in .NET

Database performance is not only about “fast queries”. It is about ensuring your application can:

  • serve more users
  • handle more concurrent requests
  • reduce latency
  • avoid lock contention
  • sustain high throughput
  • scale without rewriting architecture

To understand optimization, we must understand the core pillars of DB performance in .NET.


1.1 Where Performance Problems Normally Come From

Based on thousands of production applications, the biggest bottlenecks are:

① Too many round-trips to the database

Each DB call adds:

  • network latency
  • connection acquisition time
  • query compilation time
  • result streaming

Even if each query is fast, 100 small queries = slow API.

② Bad EF Core/LINQ usage

Examples:

  • N+1 queries
  • fetching more data than needed
  • unnecessary tracking
  • generating poor SQL
  • forcing client-side evaluation

③ Missing or incorrect indexes

Symptoms:

  • table scans
  • key lookups
  • expensive sorts
  • wrong cardinality estimates

④ Long-running transactions

Causes:

  • business logic inside a transaction
  • large SaveChanges loops
  • high isolation levels
  • poor batching

Outcome: blocking & deadlocks.

⑤ Parameter sniffing

A major but often invisible issue — one cached plan works for some parameters but becomes terrible for others.


1.2 How a .NET Request Translates Into DB Work

ASP.NET Request → DbContext → ADO.NET → SQL Server

Step 1 — Controller/Endpoint calls application logic

var orders = await _db.Orders.ToListAsync();
Code language: JavaScript (javascript)

Step 2 — EF Core translates LINQ → SQL

SELECT [o].[Id], [o].[OrderDate], [o].[CustomerId]
FROM [Orders] AS [o]
Code language: CSS (css)

Step 3 — ADO.NET submits via connection pooling

  • Gets or creates a connection
  • Sends SQL over TCP to SQL Server

Step 4 — SQL Server optimizer builds an execution plan

  • Uses statistics
  • Picks indexes
  • Determines join method

Step 5 — SQL Server executes plan

  • Reads pages from buffer pool or disk
  • Applies operators (scan, seek, join)
  • Streams results

Step 6 — Results flow back to .NET

All optimization efforts aim to reduce work in this pipeline.


1.3 Understanding the 3 Dimensions of Performance

① Latency (How fast one request finishes)

  • speed of single query
  • execution time
  • plan quality

② Throughput (How many requests can run per second)

Affected by:

  • locks
  • connection pool
  • CPU usage
  • thread starvation

③ Concurrency (How well the system performs under load)

Common issues:

  • deadlocks
  • lock escalation
  • long transactions
  • exhausted connection pools

True DB excellence means optimizing all three.


1.4 How to Measure Performance Correctly

Use:

  • SET STATISTICS IO ON
  • SQL Profiler / Extended Events
  • Query Store
  • DMVs
  • Benchmark.NET (application-side)
  • Load tests (locust, k6, wrk)

Never rely on “it feels faster”.


1.5 Example: Good vs Bad Query Flow

❌ Example: Bad LINQ

var customers = await _db.Customers.ToListAsync();

foreach (var customer in customers)
{
    var orders = await _db.Orders
        .Where(o => o.CustomerId == customer.Id)
        .ToListAsync();
}
Code language: JavaScript (javascript)

Results in 1 + N queries (N+1 Problem)
Slow at scale.

✔ Example: Good LINQ

var customers = await _db.Customers
    .Include(c => c.Orders)
    .AsNoTracking()
    .ToListAsync();
Code language: JavaScript (javascript)

One single SQL round trip.


CHAPTER 2 — Architecture of High-Performance MSSQL for .NET

This chapter explains how MSSQL works internally—not as a DBA—but exactly the parts developers need to tune performance.


2.1 Understanding SQL Server Internals (Developer Version)

SQL Server is built on these foundations:

① Relational Engine

  • Parses SQL
  • Compiles query
  • Creates execution plan
  • Reuses cached plans

② Storage Engine

  • Reads/writes data pages
  • Manages memory (buffer pool)
  • Applies locks & latches

③ Buffer Pool

  • SQL Server caches data & index pages
  • Avoids disk reads
  • Critical for performance

2.2 How the Query Optimizer Works

When EF Core sends SQL:

  1. SQL Server parses the query
  2. Optimizer considers MANY possible execution plans
  3. Uses statistics to estimate row counts
  4. Picks cheapest plan
  5. Stores plan in cache for reuse

If statistics are outdated:

→ wrong plan
→ poor performance
→ excessive scans


2.3 How EF Core Affects SQL Server Performance

EF Core impacts SQL Server through:

LINQ → SQL Translation Quality

Bad LINQ can generate terrible SQL:

  • unnecessary joins
  • client-side filtering
  • redundant subqueries

Loading strategy

  • lazy loading = many queries
  • eager/projection = efficient

Tracking

EF Core’s change tracker impacts performance on:

  • CPU
  • memory
  • caching

For read-only workloads, always use AsNoTracking().


2.4 The High-Performance .NET Database Architecture

A well-designed .NET + MSSQL architecture includes:


App Layer

  • Controllers / Endpoints
  • Services
  • Query Handlers (CQRS)
  • Using:
    • Connection pooling
    • Short lived DbContexts
    • Batch operations
    • Cancellation tokens

Data Access Layer

Options:

  • EF Core (default choice)
  • Dapper (fastest for read-heavy)
  • Stored Procedures (hot paths)
  • Hybrid DAL pattern

Database Layer

Optimized using:

  • indexes
  • updated statistics
  • tuned queries
  • proper execution plans
  • well-designed schema

Caching Layer

  • Redis
  • MemoryCache
  • Distributed cache
    Used for:
  • reference data
  • session data
  • rate limiting
  • frequently accessed datasets

(Full caching patterns will be covered in the appendices.)


2.5 Common Anti-Patterns in .NET + MSSQL Systems

❌ Long-lived DbContext (singleton)

Leads to:

  • connection leaks
  • stale tracking
  • memory bloat

❌ Storing large objects in MSSQL

  • images
  • PDFs
  • huge JSON
    Better: store in Blob Storage.

❌ SELECT * queries

Waste CPU, network, memory.

❌ Endless SaveChanges() loops

foreach (var item in items)
{
    _db.Add(item);
    await _db.SaveChangesAsync();
}
Code language: JavaScript (javascript)

✔ Fix

Perform a single bulk insert or ExecuteUpdateAsync.


2.6 Example Architecture Diagram

┌───────────────────┐      ┌─────────────────────┐     
│ ASP.NET API Layer │ ---> │ Data Access Layer   │
└───────────────────┘      │  (EF Core/Dapper)   │
                           └───────────┬─────────┘
                                       │
                            ┌──────────▼────────────┐
                            │   SQL Server Engine   │
                            │(Optimizer + Storage)  │
                            └──────────┬────────────┘
                                       │
                            ┌──────────▼────────────┐
                            │     Buffer Pool       │
                            └───────────────────────┘

2.7 What This Book Will Achieve For You

By the end, you will be able to:

  • Tune EF Core queries like a pro
  • Detect and eliminate N+1 queries
  • Read execution plans with confidence
  • Fix parameter sniffing
  • Apply correct indexing strategies
  • Design fast transactions
  • Reduce round-trips
  • Scale reads with replicas
  • Use DMVs and Query Store effectively
  • Build .NET apps that handle 10× more load

🧩 Message 2 — PART II (ORM-Level Optimization)

Chapter 3 — EF Core Query Optimization

Chapter 4 — Avoiding N+1 Queries

Chapter 5 — Query Design Optimization

Chapter 6 — Batch & Bulk Operations

Reply “continue” whenever you’re ready for Message 2.


🏎️ CHAPTER 3 — EF Core Query Optimization

EF Core is powerful, but it’s also one of the biggest sources of hidden database inefficiencies in ASP.NET applications. Its default behaviors—tracking, lazy loading, LINQ translation—can generate slow SQL, excessive round-trips, and unnecessary memory overhead.

This chapter teaches you how to write high-performance EF Core while maintaining clean architecture.


3.1 The Most Important Concept: Tracking vs. No-Tracking

Tracking mode (default)

EF Core tracks entities to detect changes during SaveChanges.

This adds:

  • CPU overhead
  • Memory overhead
  • Object graph overhead

✔ Use AsTracking() ONLY when updating

✔ Use AsNoTracking() for all read-only queries

Example:

var orders = await _db.Orders
    .AsNoTracking()
    .Where(o => o.CustomerId == id)
    .ToListAsync();
Code language: JavaScript (javascript)

Performance gains:

  • 20–40% faster query materialization
  • Lower memory usage
  • Less GC pressure

3.2 Understanding EF Core LINQ → SQL Translation

EF Core tries to convert your LINQ into SQL. But:

  • Not all LINQ translates efficiently
  • Some expressions force client-side evaluation
  • Complex expressions may produce heavy SQL

Example of BAD LINQ:

var result = _db.Orders
    .Where(x => SomeHelperMethod(x.Amount))
    .ToList();
Code language: JavaScript (javascript)

If EF cannot translate SomeHelperMethod, it downloads all rows then filters in memory.

✔ Always ensure LINQ is translatable to SQL

Keep your queries “SQL-shaped”.


3.3 Always Filter Early

Bad:

var orders = await _db.Orders
    .Include(o => o.Items)
    .ToListAsync();

var result = orders.Where(o => o.CustomerId == id);
Code language: JavaScript (javascript)

This loads all orders and items, THEN filters.

Good:

var orders = await _db.Orders
    .Where(o => o.CustomerId == id)
    .Include(o => o.Items)
    .ToListAsync();
Code language: JavaScript (javascript)

3.4 Use Projections to Reduce Data Size

Fetching full entities = heavy.
Most web APIs only need a subset.

Bad:

var orders = await _db.Orders
    .Include(o => o.Items)
    .ToListAsync();
Code language: JavaScript (javascript)

Good:

var dtos = await _db.Orders
    .Where(o => o.CustomerId == id)
    .Select(o => new OrderDto {
        Id = o.Id,
        Total = o.Items.Sum(i => i.Price),
        ItemCount = o.Items.Count
    })
    .ToListAsync();
Code language: JavaScript (javascript)

Benefits:

  • smaller results
  • less serialization time
  • fewer EF objects

3.5 Avoid Premature ToList() Calls

This is a classic mistake:

var orders = _db.Orders.ToList();
var result = orders.Where(x => x.Total > 100);
Code language: JavaScript (javascript)

This loads the entire table.

Correct:

var result = await _db.Orders
    .Where(x => x.Total > 100)
    .ToListAsync();
Code language: JavaScript (javascript)

3.6 Avoid Anonymous Includes

Bad:

context.Orders.Include("Items").ToList();
Code language: CSS (css)

Good:

context.Orders.Include(o => o.Items).ToList();
Code language: JavaScript (javascript)

String-based includes bypass compile-time safety and sometimes break eager loading rules.


3.7 Avoid Out-of-Control Includes

This is called “Include Hell”:

context.Customers
    .Include(c => c.Orders)
    .ThenInclude(o => o.Items)
    .Include(c => c.Addresses)
    .Include(c => c.Payments)
    .Include(c => c.Notes)
    ...
Code language: JavaScript (javascript)

Too many includes create monster SQL joins.

Fix:

  • Use separate queries
  • Use projections
  • Load only what you need

3.8 EF Core Logging for Performance

Enable verbose logging:

services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(connString)
           .EnableSensitiveDataLogging()
           .LogTo(Console.WriteLine);
});
Code language: JavaScript (javascript)

This exposes:

  • generated SQL
  • round-trips
  • timings

3.9 Summary

EF Core optimization focuses on:

  • Reducing tracking
  • Improving LINQ → SQL translation
  • Filtering early
  • Reducing data size
  • Avoiding N+1 queries
  • Avoiding giant includes

These principles alone can 3× performance in real apps.


🎯 End of Chapter 3


🚀 CHAPTER 4 — Avoiding N+1 Queries

One of the most common and devastating problems in ORM-based applications is the N+1 query problem. It happens when a developer unintentionally triggers one query for the parent, then one additional query per child.

This silently destroys performance.


4.1 What is N+1?

Example scenario

Suppose you want to load customers with their orders.

Bad code:

var customers = await _db.Customers.ToListAsync();

foreach (var customer in customers)
{
    var orders = await _db.Orders
        .Where(o => o.CustomerId == customer.Id)
        .ToListAsync();

    customer.Orders = orders;
}
Code language: JavaScript (javascript)

If you have 100 customers:
→ 1 query for customers
→ 100 queries for their orders

Total: 101 queries (N+1)


4.2 Why N+1 Is Catastrophic

  • Each query adds network latency
  • SQL Server receives many small calls
  • Connection pool exhaustion
  • Causes exponential slowdown under concurrency
  • Nearly impossible to scale

Even if each query is fast, 100 small queries = slow API.


4.3 How to Detect N+1

Using SQL Profiler

Watch for:

  • many repeated SELECT statements
  • identical SQL patterns
  • increasing queries during loops

Using EF Core Logging

Enable logging as shown in Chapter 3.

Using MiniProfiler

A phenomenal tool.


4.4 The Fix: Eager Loading

Correct solution:

var customers = await _db.Customers
    .Include(c => c.Orders)
    .ToListAsync();
Code language: JavaScript (javascript)

SQL generated:

SELECT c.*, o.*
FROM Customers c
LEFT JOIN Orders o ON o.CustomerId = c.Id

One query.
Zero loops.
10× performance improvement.


4.5 The Fix: Projection (Best Method)

var result = await _db.Customers
    .Select(c => new CustomerWithOrdersDto {
        Id = c.Id,
        Name = c.Name,
        Orders = c.Orders.Select(o => new OrderDto {
            OrderId = o.Id,
            Amount = o.Amount
        }).ToList()
    })
    .ToListAsync();
Code language: JavaScript (javascript)

This produces highly optimized SQL.


4.6 The Fix: Explicit Loading (rare)

var customer = await _db.Customers.FirstAsync(c => c.Id == id);
await _db.Entry(customer).Collection(c => c.Orders).LoadAsync();
Code language: JavaScript (javascript)

Use only when:

  • You load one entity
  • You control load sequence

4.7 BAD: Lazy Loading

Lazy loading is convenient, but causes automatic N+1.

Disable it:

services.AddDbContext<AppDbContext>(options =>
{
    options.UseLazyLoadingProxies(false);
});
Code language: JavaScript (javascript)

4.8 Summary

To eliminate N+1:

  1. Prefer eager loading
  2. Prefer projections
  3. Avoid lazy loading
  4. Watch for hidden loops
  5. Use profiler tools

Fixing N+1 typically yields a 5×–20× performance improvement.


🎯 End of Chapter 4


🧠 CHAPTER 5 — Query Design Optimization (SQL + LINQ)

Optimizing a database query starts long before indexing—it starts with writing good LINQ and good SQL.


5.1 Avoid SELECT *

Bad:

var orders = await _db.Orders.ToListAsync();
Code language: JavaScript (javascript)

Problems:

  • Loads unnecessary columns
  • Wastes network bandwidth
  • Wastes SQL Server CPU
  • Slows serialization

Always fetch only what you need:

var orders = await _db.Orders
    .Select(o => new { o.Id, o.Total, o.OrderDate })
    .ToListAsync();
Code language: JavaScript (javascript)

5.2 Filter Early, Filter Often

Bad:

var list = await _db.Orders
    .Include(o => o.Items)
    .ToListAsync();

var result = list.Where(o => o.OrderDate > cutoff);
Code language: JavaScript (javascript)

Good:

var result = await _db.Orders
    .Where(o => o.OrderDate > cutoff)
    .Include(o => o.Items)
    .ToListAsync();
Code language: JavaScript (javascript)

5.3 Prefer Server-Side Computation

Bad:

var orders = await _db.Orders.ToListAsync();

var high = orders.Where(o => o.Amount > 100);
Code language: JavaScript (javascript)

Good:

var high = await _db.Orders
    .Where(o => o.Amount > 100)
    .ToListAsync();
Code language: JavaScript (javascript)

5.4 Correct Pagination

Bad:

var result = await _db.Orders
    .Skip(page * size)
    .Take(size)
    .ToListAsync();
Code language: JavaScript (javascript)

This is correct but may be slow for huge offsets.

Better:

Use keyset pagination:

var result = await _db.Orders
    .Where(o => o.Id > lastId)
    .Take(size)
    .ToListAsync();
Code language: JavaScript (javascript)

5.5 Avoid Complex LINQ in a Single Expression

Bad:

var result =
    from o in _db.Orders
    where o.Customer.Name.StartsWith("A")
    select new
    {
        Total = o.Items.Sum(i => i.Price * i.Quantity)
    };
Code language: JavaScript (javascript)

This generates heavy SQL.

Better:

var result = await _db.Orders
    .Where(o => o.Customer.Name.StartsWith("A"))
    .Select(o => new {
        o.Id,
        Total = o.Items.Sum(i => i.Price * i.Quantity)
    })
    .ToListAsync();
Code language: JavaScript (javascript)

5.6 Avoid Client-Side Evaluation

Example:

var result = await _db.Orders
    .Where(o => IsPrime(o.Id)) // cannot translate to SQL
    .ToListAsync();
Code language: JavaScript (javascript)

EF Core falls back to client-side evaluation.
VERY bad.

Fix:

  • Move logic server side
  • Precompute fields
  • Use computed column
  • Use raw SQL if needed

5.7 Use Raw SQL for Complex Cases

Example:

var result = await _db.Orders
    .FromSqlInterpolated($"SELECT TOP 10 * FROM Orders WHERE ...")
    .ToListAsync();
Code language: JavaScript (javascript)

5.8 Summary

Good query design:

  • Minimal columns
  • Minimal rows
  • Server-side filtering
  • Minimal joins
  • Efficient pagination
  • SQL-shaped LINQ

This chapter alone provides 2×–10× improvement.


🎯 End of Chapter 5


💥 CHAPTER 6 — Batch Operations / Bulk Operations

This chapter covers techniques to handle large writes efficiently. SaveChanges loops are one of the biggest silent performance killers.


6.1 Why SaveChanges Loops Are Terrible

Bad:

foreach (var item in items)
{
    _db.Add(item);
    await _db.SaveChangesAsync();
}
Code language: JavaScript (javascript)

Each iteration:

  • new transaction
  • new insert
  • new log record

If you have 500 records → 500 transactions.


6.2 Solution: Batching with EF Core 7+ ExecuteUpdate

EF Core 7 introduced batch commands.

Update

await _db.Orders
    .Where(o => o.Status == Status.Pending)
    .ExecuteUpdateAsync(setters =>
        setters.SetProperty(o => o.Status, Status.Processed));
Code language: JavaScript (javascript)

Delete

await _db.Orders
    .Where(o => o.CreatedOn < cutoff)
    .ExecuteDeleteAsync();
Code language: JavaScript (javascript)

✔ No materialization
✔ One SQL statement
✔ Massively faster


6.3 Bulk Insert Options

EF Core does not provide native bulk insert yet, but there are options:

Option 1 — SqlBulkCopy (fastest)

using var bulk = new SqlBulkCopy(connection);
bulk.DestinationTableName = "Orders";
await bulk.WriteToServerAsync(dataTable);
Code language: JavaScript (javascript)

Option 2 — EFCore.BulkExtensions

await _db.BulkInsertAsync(list);
Code language: JavaScript (javascript)

Option 3 — Stored Procedure Bulk Insert


6.4 Example: 100× Faster Inserts

MethodTime for 10k rows
SaveChanges loop~30 seconds
BulkExtensions~0.5 seconds
SqlBulkCopy~0.2 seconds

6.5 Batch Reads Using Split Queries

EF Core 7+

var orders = await _db.Orders
    .Include(o => o.Items)
    .AsSplitQuery()
    .ToListAsync();
Code language: JavaScript (javascript)

Avoids giant join explosions.


6.6 Summary

For high-performance writes:

  • Avoid SaveChanges loops
  • Use ExecuteUpdate/Delete
  • Use Bulk Insert methods
  • Use Split Queries for big includes
  • Use batching


Excellent.
Here is Message 4, containing Part IV: Execution Plans & Diagnostics.

PART IV — EXECUTION PLANS & DIAGNOSTICS

Chapter 11 — Execution Plan Analysis

Chapter 12 — SQL Profiler & Query Store

Chapter 13 — DMV-Based Diagnostics

These chapters elevate you to database surgeon level — enabling you to read exactly what MSSQL is doing and why.


🔍 CHAPTER 11 — Execution Plan Analysis

Execution plans are the DNA of database performance. Every slow query reveals its secrets inside the execution plan. Being able to read plans makes you a true MSSQL performance expert.


11.1 What Is an Execution Plan?

An execution plan is the set of operations SQL Server uses to execute your query:

  • index seek
  • index scan
  • hash match
  • nested loops
  • key lookup
  • sort
  • compute scalar
  • join operations
  • spool operations
  • parallel worker distribution

It also reveals:

  • estimated vs actual row counts
  • memory usage
  • warnings
  • spills

11.2 Actual vs Estimated Plans

TypeMeaning
Estimated PlanSQL Server predicts operations before running query
Actual PlanSQL Server gives real results after execution

Always prefer Actual Execution Plan for tuning.


11.3 How to View the Execution Plan

In SSMS:

  1. Highlight query
  2. Click:
    • Ctrl + M → Include Actual Plan
  3. Execute query
  4. Switch to Execution Plan tab

11.4 Key Operators Every .NET Developer Must Know


Index Seek (excellent)

SQL Server uses an index to locate rows quickly.

Index Seek (NonClustered)

→ Fastest lookup method
→ Ideal for WHERE, JOIN


Index Scan (can be bad or okay)

SQL Server scans entire index.

Index Scan

Scanning might be fine for non-selective queries, but often indicates missing index.


Table Scan (often terrible)

Table Scan

Means no useful index.
SQL Server scans entire table pages.

→ fix by adding appropriate index.


Key Lookup (common fixable issue)

Occurs when SQL Server finds row via nonclustered index but must fetch additional columns.

Fix: Make a covering index that includes those columns.


Nested Loops Join

Fast for:

  • small outer result
  • indexed inner table

Dangerous when:

  • outer set is large
  • no index on inner table

Hash Match Join

Used when:

  • large table joins
  • no sorted input
  • stats uncertain

Needs memory → may spill.


Sort Operator

Shows SQL Server sorting results.
Sorting is expensive.

Fix:

  • Use indexed ORDER BY
  • Reduce result set

11.5 The Most Important Metric: Estimated vs Actual Rows

Example:

Estimated Rows: 1
Actual Rows: 100,000

This is called bad cardinality estimation.

Consequence:

  • wrong join type
  • spills
  • wrong index selection

Fixes:

  • update statistics
  • better indexing
  • parameter sniffing fixes

11.6 Detecting Warnings

Execution plans may show warnings like:

Missing Index

SQL suggests index improvements.
Often valid but must be reviewed manually.

Hash Warning: Spill to tempdb

Means SQL Server ran out of memory.

Fix:

  • rewrite query
  • reduce rows
  • increase work memory

Operator Using Residual Predicate

Means filter applied after seek/scan
→ potential index improvement


11.7 Example Real Plan Improvement

Original LINQ:

var orders = await _db.Orders
    .Where(o => o.Status == "Active")
    .ToListAsync();
Code language: JavaScript (javascript)

Execution Plan:

  • Table scan
  • 100k rows read
  • 2 seconds

Create index:

CREATE INDEX IX_Orders_Status ON Orders(Status);

New plan:

  • Index Seek
  • 200 rows read
  • < 50 ms

100× improvement.


11.8 Summary

Mastering execution plans enables you to:

  • find missing indexes
  • detect cardinality issues
  • diagnose poor SQL
  • solve performance regressions
  • eliminate slow queries permanently

Execution plan literacy = performance engineer power.


🎯 End of Chapter 11


📡 CHAPTER 12 — SQL Profiler & Query Store

This chapter covers runtime performance analysis, allowing you to identify slow queries, high CPU queries, and plan regressions across your application.


12.1 SQL Profiler — Real-Time View of Your App

SQL Profiler allows you to see:

  • all queries executed
  • duration
  • CPU usage
  • logical reads
  • parameter values
  • errors

Perfect for debugging .NET apps.


12.2 How to Capture EF Core Queries in Profiler

Use events:

  • RPC:Completed → stored procedure calls
  • SQL:BatchCompleted → raw SQL
  • Columns to enable:
    • Duration
    • CPU
    • Reads
    • TextData
    • LoginName
    • ApplicationName

Filter:

ApplicationName LIKE '%EntityFramework%'
Code language: JavaScript (javascript)

12.3 Detecting Slow Queries

Sort by:

  • Duration
  • Reads

Anything over:

  • 500 ms = slow
  • 100 ms = inspect
  • >10k logical reads = likely missing index

12.4 Query Store: SQL Server’s Black Box Recorder

Query Store is SQL Server’s built-in performance history database.

Tracks:

  • top CPU queries
  • longest running queries
  • regressed queries
  • execution plans
  • plan changes over time

12.5 Enabling Query Store

ALTER DATABASE MyDb SET QUERY_STORE = ON;

12.6 Key Query Store Reports

✔ Top Resource-Consuming Queries

Shows:

  • total duration
  • average duration
  • total CPU
  • total logical reads

✔ Query with High Variation

Queries slow sometimes, fast other times (parameter sniffing).

✔ Regressed Queries

Execution plan changed → performance dropped.


12.7 Forcing a Stable Plan (Fixing Bad Plans)

Find query ID, plan ID, then:

EXEC sp_query_store_force_plan @query_id, @plan_id;
Code language: CSS (css)

SQL Server now ALWAYS uses that fast plan.


12.8 Combining Profiler + Query Store

Profiler:

  • real-time
  • good for debugging

Query Store:

  • historical data
  • good for deep analysis

Together = complete performance visibility.


12.9 Summary

Profiler + Query Store gives you:

  • real-time monitoring
  • long-term insights
  • slow query detection
  • plan regression protection
  • the ability to fix slow queries permanently

🎯 End of Chapter 12


🧠 CHAPTER 13 — DMV-Based Diagnostics

DMVs (Dynamic Management Views) provide internal SQL Server metrics and performance counters — extremely valuable for diagnosing slow queries.


13.1 What Are DMVs?

DMVs expose:

  • cached queries
  • execution statistics
  • index usage
  • missing index suggestions
  • plan cache

Think of DMVs as SQL Server’s internal logs.


13.2 Find Top Expensive Queries

SELECT TOP 20 
    qs.total_worker_time AS CPU,
    qs.total_elapsed_time AS Duration,
    qs.execution_count AS Execs,
    qt.text AS QueryText
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
ORDER BY qs.total_worker_time DESC;
Code language: CSS (css)

Find:

  • CPU-heavy queries
  • slowest queries

13.3 Find Missing Indexes

SELECT 
    migs.user_seeks,
    mid.statement AS TableName,
    mid.equality_columns,
    mid.inequality_columns,
    mid.included_columns
FROM sys.dm_db_missing_index_group_stats migs
JOIN sys.dm_db_missing_index_groups mig
    ON migs.group_handle = mig.index_group_handle
JOIN sys.dm_db_missing_index_details mid
    ON mig.index_handle = mid.index_handle
ORDER BY migs.user_seeks DESC;
Code language: PHP (php)

Indexes recommended frequently = high-impact.


13.4 Find Index Usage Stats

SELECT 
    OBJECT_NAME(i.object_id) AS TableName,
    i.name AS IndexName,
    ius.user_seeks,
    ius.user_scans,
    ius.user_lookups,
    ius.user_updates
FROM sys.indexes i
LEFT JOIN sys.dm_db_index_usage_stats ius
    ON i.index_id = ius.index_id
    AND i.object_id = ius.object_id
ORDER BY ius.user_seeks DESC;
Code language: PHP (php)

If index has:

  • high updates, low seeks → remove it
  • high seeks → keep it
  • high lookups → convert to covering index

13.5 Find Queries Causing High I/O

SELECT TOP 20
    qs.total_logical_reads,
    qt.text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
ORDER BY qs.total_logical_reads DESC;
Code language: CSS (css)

High reads = expensive.


13.6 Find Queries Using TempDB (Spills)

SELECT * 
FROM sys.dm_exec_query_stats 
WHERE spilled_memory_grant_kb > 0;
Code language: CSS (css)

Meaning:

  • insufficient memory
  • bad plan
  • large dataset operations

13.7 Summary

DMVs allow deep analysis of:

  • slow queries
  • CPU consumption
  • missing indexes
  • index utilization
  • spills & memory pressure
  • plan cache patterns

This is where real SQL Server tuning mastery begins.


Great — here is Message 5, covering Part V: Application-Level Optimization.

PART V — APPLICATION-LEVEL DATABASE PERFORMANCE

Chapter 14 — Connection Pooling Optimization

Chapter 15 — Transaction Optimization

Chapter 16 — DB Command Timeout & Retry Patterns

Chapter 17 — Minimizing Network Round Trips

These chapters focus on performance issues caused not by SQL, but by how the .NET application interacts with the database.


🔌 CHAPTER 14 — Connection Pooling Optimization

Every .NET application that uses SQL Server goes through ADO.NET connection pooling, whether you’re using EF Core, Dapper, or raw SqlConnection.

Connection pooling is the #1 bottleneck in high-traffic ASP.NET applications if misconfigured.


14.1 How Connection Pooling Works

  • .NET maintains a pool of open SQL connections for each unique connection string
  • When a DbContext is used:
    1. Get a connection from pool
    2. Use it
    3. Return it
  • Opening a NEW connection is expensive (handshake, auth, TLS)
  • Reusing connections = huge time savings

14.2 Why Connection Pools Are Exhausted

Common causes:

❌ Long-running queries

Connections stay busy too long.

❌ Long-lived DbContext

Developers incorrectly register DbContext as:

  • Singleton
  • Transient but not disposed
  • Dependency stuck in long scopes

❌ Large parallel workloads

Multiple Task.Run loops without throttling.

❌ Transactions that run too long

Holding locks and connections.

❌ Async code blocking

Deadlocks or thread starvation.


14.3 The Golden Rule: Short-Lived DbContext

Correct (Scoped per request):

services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(conn));
Code language: JavaScript (javascript)

Incorrect (Singleton):

services.AddSingleton<AppDbContext>();
Code language: HTML, XML (xml)

→ This creates one connection shared across threads → disaster.


14.4 Correct Usage of DbContext

using (var db = new AppDbContext())
{
    var orders = await db.Orders.ToListAsync();
}
Code language: JavaScript (javascript)

or in ASP.NET:

public class OrdersService
{
    private readonly AppDbContext _db;

    public OrdersService(AppDbContext db)
    {
        _db = db; // scoped per request
    }
}
Code language: PHP (php)

14.5 Configuring Connection Pool Sizes

Default max pool size = 100 connections.

Increase for high-traffic apps:

"Server=...;Initial Catalog=...;Max Pool Size=200;"
Code language: JSON / JSON with Comments (json)

But note:

  • too high = SQL Server overwhelmed
  • too low = thread starvation in app

Monitor with DMVs:

SELECT * FROM sys.dm_exec_connections;
Code language: CSS (css)

14.6 Detecting Connection Leaks

Symptoms:

  • Timeout errors
  • Pool exhaustion
  • Long waits in WaitTime column

Enable logging:

options.EnableDetailedErrors()
       .EnableSensitiveDataLogging();
Code language: CSS (css)

14.7 Summary

To optimize connection pooling:

  • ALWAYS use short-lived DbContext
  • Use async/await properly
  • Avoid long-running transactions
  • Increase Max Pool Size only when needed
  • Monitor using DMVs

Connection pool optimization boosts concurrency by 2×–5×.


🎯 End of Chapter 14


🔐 CHAPTER 15 — Transaction Optimization

Transactions are essential for correctness — but deadly for performance if misused. A poorly scoped transaction will block other queries, escalate locks, and choke throughput.


15.1 What Happens During a Transaction

When a transaction starts:

  • SQL Server must guarantee ACID
  • Locks are held until commit
  • Other sessions may block
  • Log writes slow down system

This is why you must keep transactions short.


15.2 The Worst Pattern: Business Logic Inside a Transaction

Bad:

using (var tran = await _db.Database.BeginTransactionAsync())
{
    var order = await _db.Orders.FindAsync(id);

    await _paymentService.ChargeCard(order); // remote call!

    order.Status = "Paid";
    await _db.SaveChangesAsync();

    await tran.CommitAsync();
}
Code language: JavaScript (javascript)

This is a classic disaster:

  • external HTTP call inside transaction
  • transaction open for seconds
  • all other queries blocked

15.3 The Correct Pattern

var order = await _db.Orders.FindAsync(id);

await _paymentService.ChargeCard(order); // do this outside txn

using (var tran = await _db.Database.BeginTransactionAsync())
{
    order.Status = "Paid";
    await _db.SaveChangesAsync();

    await tran.CommitAsync();
}
Code language: JavaScript (javascript)

Keep transaction scope minimal.


15.4 Choosing Correct Isolation Levels

Default = ReadCommitted
Best for OLTP apps.

Other levels:

LevelUse CasePerformance
ReadCommitteddefaultgood
ReadUncommitted (NOLOCK)dirty reads OKexcellent
Snapshothigh read concurrencygood
Serializablevery strict consistencyterrible

DO NOT use Serializable unless 100% required.


15.5 Avoid Nested Transactions

EF Core does NOT handle nested transactions properly.

Bad:

using var tran1 = await _db.Database.BeginTransactionAsync();
using var tran2 = await _db.Database.BeginTransactionAsync();
Code language: JavaScript (javascript)

Avoid.


15.6 Transactions and SaveChanges

SaveChanges creates its own implicit transaction if none exists.

Thus:

❌ Long loops calling SaveChanges repeatedly

= hundreds of tiny transactions (terrible)

Fix: Batch Insert / Bulk Insert (covered earlier).


15.7 Summary

Transaction optimization means:

  • Do work OUTSIDE the transaction
  • Keep transaction scope very small
  • Use correct isolation levels
  • Avoid nested transactions
  • Avoid long SaveChanges loops

🎯 End of Chapter 15


⏱️ CHAPTER 16 — DB Command Timeout & Retry Patterns

Timeouts and retries greatly affect stability and performance — especially at scale.


16.1 The Default Timeout

By default:

  • SQL command timeout = 30 seconds
  • Bad queries will hit timeout
  • Causes cascading failures in APIs

16.2 Configuring Timeouts

EF Core:

options.UseSqlServer(conn, opt =>
    opt.CommandTimeout(60));
Code language: PHP (php)

SqlCommand:

command.CommandTimeout = 60;

16.3 The Retry Pattern (Polly)

Database calls occasionally fail:

  • deadlocks
  • transient network failures
  • failovers

Using Polly for retries:

var retry = Policy
    .Handle<SqlException>()
    .Or<TimeoutException>()
    .WaitAndRetryAsync(3, retry =>
        TimeSpan.FromMilliseconds(200 * retry));

await retry.ExecuteAsync(async () =>
{
    await _db.SaveChangesAsync();
});
Code language: JavaScript (javascript)

16.4 BUT: When Retries Make Things Worse

Retries should NOT be used for:

  • long-running queries
  • missing indexes
  • slow execution plans
  • overloaded SQL Server

In these cases, retry loops increase load and worsen outages.


16.5 Avoid Combining Timeout + Retry Blindly

Bad:

  • long timeout (120 sec)
  • many retries (5+)

This creates:

  • thread starvation
  • connection pool starvation
  • massive cascading failures

16.6 Recommended Pattern

  • CommandTimeout: 15–30 seconds
  • Retries: 3 max
  • Backoff: exponential
  • Detect root cause using Query Store

16.7 Summary

Timeouts and retries improve resilience, but must be used strategically, not as a band-aid for bad SQL.


🎯 End of Chapter 16


📡 CHAPTER 17 — Minimizing Network Round Trips

Database performance is often determined not by slow SQL, but by too many SQL calls.

Every database call has overhead:

  • TCP network latency
  • handshake
  • serializer overhead
  • connection pool operations
  • SQL Server CPU

Reducing the number of queries is a massive optimization.


17.1 Common Causes of Excess Round Trips

  • N+1 queries
  • SaveChanges loops
  • Lazy loading
  • complex includes
  • fetching data one row at a time
  • multiple sequential queries instead of batching

17.2 Combine Multiple Queries Into One

Bad:

var customers = await _db.Customers.ToListAsync();
var orders = await _db.Orders.ToListAsync();
Code language: JavaScript (javascript)

Good:

var data = await _db.Customers
    .Select(c => new {
        Customer = c,
        Orders = c.Orders
    })
    .ToListAsync();
Code language: JavaScript (javascript)

17.3 Use Split Queries for Huge Includes

var result = await _db.Orders
    .Include(o => o.Items)
    .AsSplitQuery() // avoids giant join
    .ToListAsync();
Code language: JavaScript (javascript)

17.4 Use Stored Procedures for Multi-Step Logic

Instead of:

var stats = await _db.Orders.Where(...).CountAsync();
var totals = await _db.Orders.Where(...).SumAsync();
Code language: JavaScript (javascript)

Use a single SP:

CREATE PROCEDURE GetOrderStats
AS
BEGIN
    SELECT COUNT(*), SUM(Total) FROM Orders;
END
Code language: PHP (php)

17.5 Use Caching to Avoid DB Calls Entirely

Cache:

  • reference data
  • lookup tables
  • dropdown lists
  • rarely-changing data

Use Redis or MemoryCache.


17.6 Use Projections to Reduce Result Size

Smaller payload = faster network, faster serialization.


17.7 Summary

Minimizing round trips:

  • improves latency
  • reduces server load
  • improves scalability
  • increases concurrency
  • reduces connection pool stress

Great — here is Message 6, covering Part VI (Scaling Beyond a Single Database) and Part VII (AI-Assisted Optimization).

This message includes:

PART VI — SCALING BEYOND A SINGLE DATABASE

Chapter 18 — Read-Replica / Read Scaling Patterns

Chapter 19 — Distributed Caching & Hot Data (Bonus)

PART VII — AI-ASSISTED OPTIMIZATION

Chapter 20 — AI-Assisted Query Optimization

These chapters teach how to scale beyond a single SQL Server instance and how to use modern AI tools to elevate database tuning.


🌐 CHAPTER 18 — Read-Replica / Read Scaling Patterns

When your application grows, the first bottleneck you hit is usually read-heavy workloads. SQL Server supports several strategies to scale reads horizontally so that a single primary database does not become overwhelmed.

This chapter explains how .NET apps can offload reads to replicas safely.


18.1 What Is Read Scaling?

In a typical architecture:

  • The primary database handles writes
  • One or more read replicas (synchronous or asynchronous) handle SELECT queries

This offloads read load from the primary, allowing it to focus on transactional integrity.


18.2 When Read-Replicas Are Needed

If you observe:

  • High read latency
  • CPU on SQL Server consistently above 70%
  • Many SELECT queries with high concurrency
  • Dashboards and reporting queries spiking load
  • API endpoints frequently reading the same data

→ You need read scaling.


18.3 SQL Server Read Scaling Options

Always On Availability Groups (AG)

  • Most enterprise-ready
  • Provides synchronous/asynchronous replicas
  • Read-only routing
  • Automatic failover

Transactional Replication

  • Good for reporting databases
  • Flexible
  • More setup

Log Shipping (not ideal for reads)

  • Used for disaster recovery
  • Not good for real-time reads

Scaling with Azure SQL (PaaS)

  • Readable secondaries built-in
  • Geo-redundant replicas

18.4 How .NET Routes Reads to Replicas

In EF Core, you can configure read/write splitting by using two different connection strings:

services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(primaryConnString);
});

services.AddDbContext<ReadOnlyDbContext>(options =>
{
    options.UseSqlServer(readReplicaConnString);
});
Code language: JavaScript (javascript)

Usage:

public class CustomerService
{
    private readonly ReadOnlyDbContext _readDb;
    private readonly AppDbContext _writeDb;

    public CustomerService(ReadOnlyDbContext readDb, AppDbContext writeDb)
    {
        _readDb = readDb;
        _writeDb = writeDb;
    }

    public Task<List<Customer>> GetCustomersAsync() =>
        _readDb.Customers.AsNoTracking().ToListAsync();

    public Task AddCustomer(Customer c) =>
        _writeDb.Customers.AddAsync(c);
}
Code language: PHP (php)

18.5 Read Routing in SQL Server (Availability Groups)

SQL Server can automatically route read-only workloads to replicas using:

ALTER AVAILABILITY GROUP [MyAG]
MODIFY REPLICA ON N'MyReplica'
WITH (SECONDARY_ROLE (ALLOW_CONNECTIONS = READ_ONLY));
Code language: JavaScript (javascript)

Then configure client driver:

"Server=tcp:primary-db;ApplicationIntent=ReadOnly;"
Code language: JSON / JSON with Comments (json)

18.6 Replica Lag — The Hidden Danger

Asynchronous replicas often lag behind the primary by:

  • milliseconds
  • seconds
  • sometimes minutes

This creates eventual consistency, meaning:

Your app might read stale data.

Therefore:

Use replicas only for:

  • dashboards
  • reports
  • queries not requiring up-to-the-second accuracy

DO NOT use replicas for:

  • login checks
  • shopping carts
  • financial transactions

18.7 Summary

Read-scaling allows:

  • massive concurrency
  • lower latency for reads
  • offloading dashboards/reports
  • increased failover and resilience

Correctly implemented, it can increase read throughput by 5×–20×.


🎯 End of Chapter 18


CHAPTER 19 — Distributed Caching & Hot Data (Bonus Chapter)

Although not strictly a database feature, caching is one of the strongest performance tools any .NET application can use to reduce database load.


19.1 When to Use Caching

Caching is ideal for:

  • reference data (countries, roles, plans)
  • dashboards
  • frequently accessed but rarely changing data
  • expensive queries
  • read-heavy APIs

Effects:

  • reduces DB round-trips
  • lowers latency
  • increases throughput
  • reduces connection pool pressure

19.2 Types of Caches

MemoryCache (in-process)

Fastest, but not shared across servers.

Redis Distributed Cache

Best for:

  • load-balanced environments
  • microservices
  • multi-node clusters
  • storing shared state

Hybrid caching

Use MemoryCache as local L1 and Redis as L2.


19.3 Caching Database Query Results

Example using MemoryCache:

public class ProductService
{
    private readonly IMemoryCache _cache;
    private readonly AppDbContext _db;

    public ProductService(IMemoryCache cache, AppDbContext db)
    {
        _cache = cache;
        _db = db;
    }

    public async Task<List<Product>> GetProductsAsync()
    {
        return await _cache.GetOrCreateAsync("products", async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
            return await _db.Products.AsNoTracking().ToListAsync();
        });
    }
}
Code language: PHP (php)

19.4 Cache Invalidation

“Cache invalidation is one of the hardest problems in computer science.”

Approaches:

Time-based expiration (TTL)

Simple, robust.

Event-driven invalidation

When a record changes, update/remove cache.

Version tokens

Cache invalidated when version increments.


19.5 What Not to Cache

Avoid caching:

  • large results (>1 MB)
  • volatile data (frequently updated)
  • personalized data (not reusable)

19.6 Summary

Distributed caching offloads massive amounts of read traffic from the database and improves latency dramatically.

Caching is not optional — it is essential for scalable systems.


🎯 End of Chapter 19


🤖 CHAPTER 20 — AI-Assisted Query Optimization

AI tools (Copilot, ChatGPT, SQL Prompt AI, Azure Data Studio Copilot) can dramatically accelerate SQL tuning — if used correctly.

This chapter shows how to integrate AI safely into database optimization workflows.


20.1 What AI Can Do for SQL Developers

AI tools can:

  • explain execution plans
  • rewrite inefficient queries
  • suggest indexing strategies
  • identify anti-patterns
  • generate optimized LINQ
  • translate LINQ ↔ SQL
  • detect N+1 patterns
  • provide missing index recommendations
  • rewrite procedural SQL into set-based SQL

AI is now a powerful assistant — not a replacement for expertise.


20.2 Using AI to Explain Execution Plans

You can paste the plan XML or screenshot into AI:

Example Prompt:

Explain why this query is slow and how to optimize it.

AI output typically analyzes:

  • scans
  • key lookups
  • joins
  • cardinality issues
  • spill warnings

This saves hours of manual investigation.


20.3 Using AI to Rewrite SQL

Example slow query:

SELECT *
FROM Orders
WHERE YEAR(OrderDate) = 2024;

AI suggested rewrite:

SELECT *
FROM Orders
WHERE OrderDate >= '2024-01-01'
  AND OrderDate <  '2025-01-01';
Code language: JavaScript (javascript)

Now index can be used.


20.4 AI-Assisted Index Tuning

Example prompt:

Suggest indexes to improve performance for this query. Provide reasoning.

AI can recommend:

CREATE INDEX IX_Orders_OrderDate ON Orders (OrderDate) INCLUDE (Amount, Status);
Code language: PHP (php)

But always verify using:

  • execution plans
  • Query Store
  • benchmarks

20.5 Using AI to Improve EF Core LINQ

Original LINQ:

var data = _db.Orders
    .Where(o => o.Customer.Name.Contains(filter))
    .ToList();
Code language: JavaScript (javascript)

AI rewrite:

var data = _db.Orders
    .Where(o => o.Customer.Name.StartsWith(filter))
    .Select(o => new OrderDto { Id = o.Id, Total = o.Total })
    .AsNoTracking()
    .ToList();
Code language: JavaScript (javascript)

Better SQL:

  • uses index
  • fewer columns
  • no navigation property translation pitfalls

20.6 AI Tools Integrated with SQL Server

✔ Azure Data Studio Copilot

  • Explains queries
  • Suggests improvements

✔ GitHub Copilot in VS Code

  • Helps with LINQ → SQL translation
  • Suggests optimized queries

✔ Redgate SQL Prompt AI

  • SQL rewriting
  • best practices checks

20.7 The Dangers of Blindly Trusting AI

AI can:

  • propose incorrect indexes
  • rewrite queries incorrectly
  • misunderstand data models
  • produce dangerous suggestions (NOLOCK everywhere)

Always validate via execution plans.


20.8 Summary

AI is a multiplier for SQL developers:

  • speeds up analysis
  • accelerates optimization
  • improves SQL & LINQ quality
  • helps avoid common mistakes

Combined with your knowledge from earlier chapters, AI becomes an unfair advantage.


Here is Message 7, the final installment, containing:

APPENDICES + FINAL BOOK COMPILATION

Appendix A — Performance Anti-Patterns Checklist

Appendix B — Database Tuning Script Library

Appendix C — Benchmarking Template (for .NET + MSSQL)

Appendix D — Before & After Case Studies

Final Notes & How to Use This Book

This completes the full book.


📎 APPENDIX A — PERFORMANCE ANTI-PATTERNS CHECKLIST

This is a one-stop checklist of the most common (and damaging) mistakes in .NET + MSSQL applications. Use it during code reviews and performance audits.


A.1 EF Core Anti-Patterns

❌ Using DbContext as Singleton
❌ Using Lazy Loading in API applications
❌ Calling ToList() too early
❌ Selecting entire entities when only DTOs needed
❌ Using .Result or .Wait() causing deadlocks
❌ Using client-side evaluation
❌ Loading large graphs with too many .Include()
❌ Using navigation properties inside loops
❌ Not using AsNoTracking() for reads
❌ Using LINQ methods not translatable to SQL


A.2 SQL Anti-Patterns

❌ SELECT *
❌ Missing indexes on foreign keys
❌ Functions on indexed columns (e.g., WHERE YEAR(DateCol) = 2024)
❌ Using table variables for large temp data
❌ Using cursors
❌ Scalar UDFs in WHERE
❌ Over-indexing tables
❌ Joining on non-indexed columns
❌ OR conditions instead of UNION ALL
❌ Large OFFSET/FETCH without keyset pagination


A.3 Application-Level Anti-Patterns

❌ Long-running transactions
❌ External service calls inside transactions
❌ Keeping DbContext alive too long
❌ Insufficient connection pool size
❌ High number of parallel tasks without throttling
❌ Excessive retries causing cascading failures
❌ No caching for rarely changing data
❌ Chatty APIs triggering many small DB calls


A.4 Infrastructure Anti-Patterns

❌ Using only a single SQL instance under heavy load
❌ No read replicas
❌ No SQL monitoring/alerting
❌ No index maintenance
❌ Ignoring Query Store regressions
❌ No automatic statistics updates


A.5 Parameter Sniffing Anti-Patterns

❌ Relying on cached plan for highly selective queries
❌ NOT using OPTIMIZE FOR UNKNOWN
❌ Using SPs without considering parameter variance


🎯 Appendix A Summary

Treat this checklist as your pre-release audit for every performance-critical application.


📘 APPENDIX B — DATABASE TUNING SCRIPT LIBRARY

Below are the most valuable scripts every .NET performance engineer should use.


B.1 Top CPU Queries

SELECT TOP 20 
    qs.total_worker_time AS TotalCPU,
    qs.total_worker_time / qs.execution_count AS AvgCPU,
    qs.execution_count AS ExecCount,
    qt.text AS SQLText
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
ORDER BY qs.total_worker_time DESC;
Code language: PHP (php)

B.2 Top I/O Queries

SELECT TOP 20 
    qs.total_logical_reads AS Reads,
    qs.total_logical_reads / qs.execution_count AS AvgReads,
    qt.text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
ORDER BY qs.total_logical_reads DESC;
Code language: PHP (php)

B.3 Missing Indexes

SELECT 
    mid.statement AS TableName,
    mid.equality_columns,
    mid.inequality_columns,
    mid.included_columns,
    migs.user_seeks,
    migs.avg_total_user_cost
FROM sys.dm_db_missing_index_details mid
JOIN sys.dm_db_missing_index_groups mig
    ON mid.index_handle = mig.index_handle
JOIN sys.dm_db_missing_index_group_stats migs
    ON mig.index_group_handle = migs.group_handle
ORDER BY migs.user_seeks DESC;
Code language: PHP (php)

B.4 Index Usage (Find Unused Indexes)

SELECT 
    OBJECT_NAME(i.object_id) AS TableName,
    i.name AS IndexName,
    ius.user_seeks, ius.user_scans,
    ius.user_lookups, ius.user_updates
FROM sys.indexes i
LEFT JOIN sys.dm_db_index_usage_stats ius
    ON i.object_id = ius.object_id AND i.index_id = ius.index_id
WHERE OBJECTPROPERTY(i.object_id, 'IsUserTable') = 1
ORDER BY ius.user_updates DESC;
Code language: PHP (php)

Indexes with zero seeks but many updates should be removed.


B.5 Detecting Plan Regression

SELECT 
    q.query_id,
    p.plan_id,
    rs.avg_duration,
    rs.last_execution_time
FROM sys.query_store_query AS q
JOIN sys.query_store_plan AS p 
    ON q.query_id = p.query_id
JOIN sys.query_store_runtime_stats AS rs 
    ON p.plan_id = rs.plan_id
ORDER BY rs.avg_duration DESC;
Code language: PHP (php)

B.6 Detecting TempDB Spills

SELECT 
    qs.plan_handle, 
    qs.total_spills, 
    qt.text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
WHERE qs.total_spills > 0;
Code language: CSS (css)

🎯 Appendix B Summary

These scripts alone can solve 80% of SQL performance issues.


🧪 APPENDIX C — Benchmarking Template (for .NET + MSSQL)

Benchmarking database performance involves three components:

  1. Workload Generator
  2. Application Logging & Metrics
  3. SQL Server Statistics

C.1 Load Test Setup

Use one of:

  • k6
  • Locust
  • JMeter
  • Artillery

Example k6 script:

import http from 'k6/http';
import { sleep } from 'k6';

export let options = {
  vus: 20,
  duration: '30s',
};

export default function () {
  http.get('http://localhost:5000/api/orders');
  sleep(1);
}
Code language: JavaScript (javascript)

C.2 Application Metrics to Capture

Add middleware to measure:

  • response time
  • SQL query count
  • SQL execution time
  • connection pool usage
  • memory consumption
  • GC activity

Example:

app.Use(async (context, next) =>
{
    var sw = Stopwatch.StartNew();
    await next.Invoke();
    sw.Stop();

    Console.WriteLine($"Request took {sw.ElapsedMilliseconds} ms");
});
Code language: JavaScript (javascript)

C.3 SQL Metrics to Capture

Use Query Store or DMVs to measure:

  • CPU
  • Logical reads
  • Duration
  • Execution count

C.4 Before/After Comparison Template

MetricBeforeAfter
Avg Query Time500 ms60 ms
Logical Reads35,0001,200
CPU %90%20%
Requests/sec50350

🎯 Appendix C Summary

Benchmarking is essential to prove performance gains and avoid regressions.


📊 APPENDIX D — Before & After Case Studies

These real-world examples show how tuning changes transform performance.


D.1 Case Study: Eliminating N+1 Queries

Before:

  • 1 parent query + 100 child queries
  • Avg execution: 1.8 seconds

After:

  • Single query with .Include()
  • Avg execution: 120 ms

15× improvement


D.2 Case Study: Adding Covering Index

Query:

SELECT OrderDate, Amount 
FROM Orders 
WHERE CustomerId = 10;

Added:

CREATE INDEX IX_Orders_Cust ON Orders(CustomerId)
INCLUDE (OrderDate, Amount);
Code language: PHP (php)

Before:

  • Table scan
  • 2,100,000 rows
  • 900 ms

After:

  • Index seek
  • 120 rows
  • 10 ms

90× improvement


D.3 Case Study: Fixing Parameter Sniffing

Added:

OPTION (OPTIMIZE FOR UNKNOWN)

Before:

  • 4 seconds on large customers
  • 20 ms on small customers

After:

  • Stable 30 ms across ALL customers

D.4 Case Study: Rewrite WHERE Clause

Original:

WHERE YEAR(OrderDate) = 2024

Rewrite:

WHERE OrderDate >= '2024-01-01'
AND OrderDate < '2025-01-01'
Code language: JavaScript (javascript)

Before:

  • Index unusable
  • ~600 ms

After:

  • Index seek
  • ~20 ms

D.5 Case Study: Bulk Insert

Loop with SaveChanges:

  • 10k rows
  • 25 seconds

Using SqlBulkCopy:

  • 10k rows
  • 0.2 seconds


🎯 Appendix D Summary

Real-world results demonstrate how tuning changes can produce order-of-magnitude improvements.


🏁 FINAL NOTES — How to Use This Book

Congratulations — you now have one of the most complete, modern, enterprise-grade guides ever written on Database Performance Optimization in .NET + MSSQL.

To apply it effectively:


1 — Start at the ORM Layer (Cheapest Fixes)

Most performance issues come from:

  • bad LINQ
  • unoptimized EF Core usage
  • N+1 queries
  • excessive includes
  • poor loading strategies

Fixing ORM issues alone can improve performance by 5×–20×.


2 — Then Fix SQL Query Shape

Use:

  • projections
  • proper filtering
  • keyset pagination
  • batching
  • proper JOINs

3 — Then Fix Indexing & Plans

Use:

  • execution plan analysis
  • Query Store
  • DMVs

Indexes deliver 10×–100× improvements.


4 — Then Fix Application Patterns

Address:

  • connection pool
  • caching
  • retry logic
  • transaction scope

5 — Finally, Scale Outwards

Use:

  • read replicas
  • caching
  • microservices
  • distributed systems concepts

6 — Use AI as a Force Multiplier

AI is extraordinary for:

  • debugging execution plans
  • rewriting SQL
  • optimizing EF Core
  • generating indexes

But ALWAYS validate.


7 — Re-Benchmark Often

Performance regressions are inevitable.
Repeat benchmarking after:

  • new features
  • schema changes
  • SQL patches
  • traffic increases

Subscribe
Notify of
guest
0 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments

Certification Courses

DevOpsSchool has introduced a series of professional certification courses designed to enhance your skills and expertise in cutting-edge technologies and methodologies. Whether you are aiming to excel in development, security, or operations, these certifications provide a comprehensive learning experience. Explore the following programs:

DevOps Certification, SRE Certification, and DevSecOps Certification by DevOpsSchool

Explore our DevOps Certification, SRE Certification, and DevSecOps Certification programs at DevOpsSchool. Gain the expertise needed to excel in your career with hands-on training and globally recognized certifications.

0
Would love your thoughts, please comment.x
()
x