<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: azhadsuhaimi</title>
    <description>The latest articles on DEV Community by azhadsuhaimi (@azhadsuhaimi).</description>
    <link>https://dev.to/azhadsuhaimi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4066913%2F96d9bed1-8528-4df9-9ca7-48770c96bc10.png</url>
      <title>DEV Community: azhadsuhaimi</title>
      <link>https://dev.to/azhadsuhaimi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/azhadsuhaimi"/>
    <language>en</language>
    <item>
      <title>Building NetPulse: A Production-Ready .NET 8 + Next.js SaaS Boilerplate</title>
      <dc:creator>azhadsuhaimi</dc:creator>
      <pubDate>Fri, 28 Aug 2026 16:34:26 +0000</pubDate>
      <link>https://dev.to/azhadsuhaimi/building-netpulse-a-production-ready-net-8-nextjs-saas-boilerplate-273e</link>
      <guid>https://dev.to/azhadsuhaimi/building-netpulse-a-production-ready-net-8-nextjs-saas-boilerplate-273e</guid>
      <description>&lt;p&gt;Over the last 1–2 months, I stopped chasing new side-project ideas and decided to solve a frustration I’ve had for a long time: &lt;strong&gt;The exhausting setup phase before writing actual business logic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As part of my ongoing engineering work at &lt;a href="https://pulselabsmy.com/" rel="noopener noreferrer"&gt;PulseLabs&lt;/a&gt;, I frequently test and prototype product ideas. But every single time, I found myself spending 3–4 weeks repeating the exact same setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configuring OAuth callback handling&lt;/li&gt;
&lt;li&gt;Designing Entity Framework models and running PostgreSQL migrations&lt;/li&gt;
&lt;li&gt;Handling JWT access &amp;amp; refresh tokens securely&lt;/li&gt;
&lt;li&gt;Wiring up payment gateways and handling brittle webhooks&lt;/li&gt;
&lt;li&gt;Building Docker environments that actually work locally and in production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the time the app foundation was ready, the initial momentum was gone. &lt;/p&gt;

&lt;p&gt;So, I spent the last two months building &lt;strong&gt;NetPulse&lt;/strong&gt;—an enterprise-grade, clean-architecture SaaS boilerplate designed to crush that setup phase down to under 5 minutes.&lt;/p&gt;

&lt;p&gt;Here is what I focused on solving during this build, how I structured the project, and the architectural decisions I made along the way.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. Solving "Multi-Provider Payment Lock-In"
&lt;/h3&gt;

&lt;p&gt;Most SaaS templates lock you into a single payment gateway (usually just Stripe). But depending on where your customers are or how you handle international taxes/MoR (Merchant of Record), you might prefer &lt;strong&gt;Polar.sh&lt;/strong&gt; or &lt;strong&gt;Lemon Squeezy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of hardcoding Stripe everywhere, I engineered a &lt;strong&gt;Multi-Gateway Billing Engine&lt;/strong&gt; in the backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;               ┌──────────────────────┐
               │  IBillingService     │ (Abstraction Layer)
               └──────────┬───────────┘
                          │
     ┌────────────────────┼────────────────────┐
     ▼                    ▼                    ▼
[ StripeProvider ]  [ PolarProvider ]  [ LemonSqueezyProvider ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whether a subscription event (Free, Pro, Enterprise) comes from Stripe, Polar, or Lemon Squeezy, the API standardizes subscription lifecycles, checkout sessions, and webhook processing seamless inside ASP.NET Core.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Tech Stack: Clean Architecture + Modern Frontend
I wanted a stack that offers blazing performance, type safety, and zero bloat:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Backend: ASP.NET Core (.NET 8 Web API) using Clean Architecture (Domain, Application, Infrastructure, and WebApi presentation layers).&lt;/p&gt;

&lt;p&gt;Database &amp;amp; ORM: PostgreSQL 16 + Entity Framework Core 8 (Npgsql) with code-first migrations and connection pooling.&lt;/p&gt;

&lt;p&gt;Frontend: Next.js (App Router, React 18, TypeScript, Tailwind CSS, Shadcn UI / Radix primitives).&lt;/p&gt;

&lt;p&gt;Auth &amp;amp; Security: ASP.NET Core Identity + JWT Bearer tokens + Google &amp;amp; GitHub OAuth 2.0. Integrated with Audit Logging, Rate Limiting, and Global Exception middleware.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The "One-Command Setup" Nightmare (Solved with Docker)
Setting up a full-stack environment with .NET, Node, PostgreSQL, and local webhook tunnels manually is a headache for developers cloning a starter kit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I packaged the entire ecosystem into a single docker-compose.yml setup. Running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boots up the Next.js frontend, .NET 8 Web API, PostgreSQL 16 DB, and an automated Stripe CLI webhook tunnel container simultaneously.&lt;/p&gt;

&lt;p&gt;Here’s a quick glance at the repository structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NetPulse/
├── client/          # Next.js App Router Frontend
├── src/             # .NET 8 Backend Solution
│   ├── Core/        # Domain &amp;amp; Application Business Logic
│   └── WebApi/      # Controllers, Middlewares &amp;amp; Config
├── docker-compose.yml
└── NetPulse.sln
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key Lessons Learned from 2 Months of Building&lt;br&gt;
Separation of Concerns saves sanity: Keeping payment provider integrations inside the Infrastructure layer means swapping gateways doesn't touch the core business logic.&lt;/p&gt;

&lt;p&gt;Developer Experience (DX) matters most: A boilerplate shouldn't just be "code"—it needs step-by-step documentation, working .env.example templates, and zero-headache local deployment.&lt;/p&gt;

&lt;p&gt;I’d Love Your Feedback! 💬&lt;br&gt;
As I finalize version 1.0 of NetPulse, I’d love to get insights from fellow .NET devs, Next.js builders, and SaaS founders:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Payment Gateways: Do you prefer using traditional processors like Stripe, or Merchant of Record (MoR) platforms like Polar.sh / Lemon Squeezy for your SaaS?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Backend Architecture: Do you prefer strict Clean Architecture (4 layers) for SaaS templates, or a Vertical Slice Architecture approach?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What is the single most annoying feature you hate setting up manually in every new project?&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Drop your thoughts in the comments below! I'll be active in the thread replying to architecture questions. 👇&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>nextjs</category>
      <category>saas</category>
    </item>
    <item>
      <title>I Spent 30+ Hours Building "The Boring SaaS Stack" So I Never Have To Do It Again</title>
      <dc:creator>azhadsuhaimi</dc:creator>
      <pubDate>Tue, 18 Aug 2026 02:13:05 +0000</pubDate>
      <link>https://dev.to/azhadsuhaimi/i-spent-30-hours-building-the-boring-saas-stack-so-i-never-have-to-do-it-again-pa9</link>
      <guid>https://dev.to/azhadsuhaimi/i-spent-30-hours-building-the-boring-saas-stack-so-i-never-have-to-do-it-again-pa9</guid>
      <description>&lt;p&gt;We’ve all been there: &lt;/p&gt;

&lt;p&gt;You get an exciting idea for a micro-SaaS on a Friday night. You fire up your IDE, ready to build the core feature that solves a real problem. &lt;/p&gt;

&lt;p&gt;Fast forward 3 weeks later... and you haven't even written a single line of business logic yet. &lt;/p&gt;

&lt;p&gt;Instead, you’ve spent your precious free time configuring Google OAuth, wrestling with Stripe webhook signatures, building user profile pages, and tweaking Dark Mode CSS variables for the 100th time.&lt;/p&gt;

&lt;p&gt;By the time the "boring 80%" of the app is finally functional, the initial excitement is gone, burnout hits, and another side project dies silently in a local directory.&lt;/p&gt;




&lt;h3&gt;
  
  
  Taming the "Boring 80%"
&lt;/h3&gt;

&lt;p&gt;I got tired of repeating this painful cycle every time I wanted to test a new product idea. So, I decided to freeze new feature builds for a moment and focus entirely on creating a clean, unbloated foundation.&lt;/p&gt;

&lt;p&gt;My goal was simple: &lt;strong&gt;Build a lean, production-ready setup once, and reuse it forever.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is the lightweight stack and core architecture I put together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; ASP.NET Core Web API (Clean, predictable, and fast execution)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth Layer:&lt;/strong&gt; Social Sign-In (Google &amp;amp; GitHub OAuth) to eliminate email/password management overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing:&lt;/strong&gt; Stripe integration handling subscription checkouts (Pro / Enterprise tiers) and webhook listener setups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI/UX:&lt;/strong&gt; Responsive Dashboard, basic credential profile management, and native Light/Dark theme toggling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a quick high-level look at how I kept the user context and session management minimal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Keeping auth payload lean while preserving claim context&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CurrentUserContext&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;UserId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;SubscriptionTier&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Free"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsActive&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SubscriptionTier&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"Free"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Big Dilemma: How Much "Boilerplate" Is Too Much?&lt;br&gt;
While structuring this setup, I noticed a huge trap in existing commercial boilerplates: Over-engineering.&lt;/p&gt;

&lt;p&gt;Many templates try to throw in every single library under the sun—multi-tenancy, complex RBAC permissions, 5 different payment gateways, blog engines, and heavy microservice abstractions. You end up spending more time deleting code you don't need than actually building your app.&lt;/p&gt;

&lt;p&gt;I wanted to keep mine as stripped-down as possible while still covering the actual launch requirements.&lt;/p&gt;

&lt;p&gt;I'd Love Your Input! 💬&lt;br&gt;
Before I finalize version 1.0 of this foundation for my upcoming micro-SaaS launches, I want to hear from fellow builders and backend devs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Multi-Tenancy vs. Single-User: Is built-in Organization/Team management an absolute MUST-HAVE for v1, or is simple single-user auth enough for an initial MVP?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Payment Integrations: Do you prefer sticking exclusively to Stripe, or is LemonSqueezy/Paddle support a dealbreaker for global taxes?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What is the single most annoying thing you usually hate about 3rd-party starter kits/boilerplates?&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Drop your thoughts, hot takes, or architectural advice in the comments below! 👇&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>webdev</category>
      <category>saas</category>
    </item>
    <item>
      <title>The Silent Killer of ASP.NET Core Performance: The N+1 Query Problem</title>
      <dc:creator>azhadsuhaimi</dc:creator>
      <pubDate>Thu, 13 Aug 2026 03:26:10 +0000</pubDate>
      <link>https://dev.to/azhadsuhaimi/the-silent-killer-of-aspnet-core-performance-the-n1-query-problem-1akc</link>
      <guid>https://dev.to/azhadsuhaimi/the-silent-killer-of-aspnet-core-performance-the-n1-query-problem-1akc</guid>
      <description>&lt;p&gt;It’s a classic story: &lt;/p&gt;

&lt;p&gt;You build an API endpoint in ASP.NET Core using Entity Framework Core. You test it locally with 10 dummy records in your database, and it responds in a lightning-fast &lt;strong&gt;15 milliseconds&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;You push it to production. A few weeks later, as the user base and data grow, users start complaining that the app is loading painfully slow. You check your APM logs, and to your horror, a single HTTP GET request is triggering &lt;strong&gt;1,001 SQL queries&lt;/strong&gt; to the database.&lt;/p&gt;

&lt;p&gt;Welcome to the &lt;strong&gt;N+1 Query Problem&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  What Actually Happens Under the Hood?
&lt;/h3&gt;

&lt;p&gt;The N+1 problem occurs when your application executes &lt;strong&gt;1 query&lt;/strong&gt; to fetch a parent record (or list of records), and then executes &lt;strong&gt;N additional queries&lt;/strong&gt; to fetch related child data for every single parent item in that list.&lt;/p&gt;

&lt;p&gt;Consider this innocent-looking LINQ code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1 Query to fetch 100 active customers&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customers&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsActive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// N Queries executed inside the loop!&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;latestOrder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Process order...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have 100 customers:&lt;/p&gt;

&lt;p&gt;1 Query fetches the list of customers.&lt;/p&gt;

&lt;p&gt;100 Queries are fired inside the foreach loop to get each customer's order.&lt;/p&gt;

&lt;p&gt;Total database roundtrips = 101. Multiply that by network latency, and your database connection pool starts crying for help.&lt;/p&gt;

&lt;p&gt;How to Catch It Before It Hits Production&lt;br&gt;
EF Core makes lazy loading or lazy queries inside loops deceptively easy. Here are three ways to hunt down N+1 queries in your codebase:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enable SQL Logging in Development
In your appsettings.Development.json, set EF Core logging to Information:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"Logging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"LogLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Microsoft.EntityFrameworkCore.Database.Command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Watch your terminal output. If you see a wall of identical SELECT statements scrolling past for a single request, you've got an N+1 issue.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Throw Exceptions on Unintended Queries
If you use Lazy Loading, you can explicitly configure EF Core to throw an exception in development whenever a query is triggered implicitly:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;optionsBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSqlServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureWarnings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warnings&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; 
        &lt;span class="n"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Throw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RelationalEventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MultipleCollectionIncludeWarning&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The Fix: Eager Loading &amp;amp; Projection&lt;br&gt;
Instead of fetching data inside loops, tell EF Core exactly what related data you need up-front so it generates a single JOIN, or project directly into a DTO.&lt;/p&gt;

&lt;p&gt;Approach A: Eager Loading (Include)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Generates a SINGLE SQL JOIN query&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;customersWithOrders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customers&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsActive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Approach B: Direct Projection (Best Performance)&lt;br&gt;
Only fetch the fields your API actually returns. This avoids fetching unneeded columns and forces EF Core to construct an optimized SQL query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;customerDtos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customers&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsActive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CustomerDto&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;CustomerId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CustomerName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;LatestOrderDate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conclusion&lt;br&gt;
N+1 queries rarely show up as errors—they just quietly degrade your performance as your database scales. By adopting direct DTO projection and monitoring SQL logs during local development, you can catch these bottlenecks before your production database takes a hit.&lt;/p&gt;

&lt;p&gt;How do you usually catch hidden ORM query issues in your team? Do you rely on APM tools, EF Core logging, or static analysis tools?&lt;/p&gt;

&lt;p&gt;Let me know in the comments below!&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>webdev</category>
      <category>database</category>
    </item>
    <item>
      <title>Why Is My SQL Query Slow Only in Production? (The Parameter Sniffing Trap)</title>
      <dc:creator>azhadsuhaimi</dc:creator>
      <pubDate>Mon, 10 Aug 2026 01:08:43 +0000</pubDate>
      <link>https://dev.to/azhadsuhaimi/why-is-my-sql-query-slow-only-in-production-the-parameter-sniffing-trap-1ki9</link>
      <guid>https://dev.to/azhadsuhaimi/why-is-my-sql-query-slow-only-in-production-the-parameter-sniffing-trap-1ki9</guid>
      <description>&lt;p&gt;It’s every developer's favorite Friday afternoon nightmare:&lt;/p&gt;

&lt;p&gt;A user complains that a feature in the app is hanging. You take the exact SQL query executed by the application, paste it into SQL Server Management Studio (SSMS), hit Execute... and it finishes in &lt;strong&gt;0.02 seconds&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;You run it again. Blazing fast. &lt;/p&gt;

&lt;p&gt;Yet, inside the application, it continues to time out. &lt;/p&gt;

&lt;p&gt;If you’ve been building database-backed apps long enough, you’ve almost certainly run into &lt;strong&gt;Parameter Sniffing&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  What Actually Happens Under the Hood?
&lt;/h3&gt;

&lt;p&gt;When a parameterized query or Stored Procedure runs for the very first time, SQL Server looks at the parameters passed in at that specific moment. It uses those values to estimate how many rows will be returned and compiles an execution plan tailored for that payload.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scenario A:&lt;/strong&gt; The first run passes a parameter that returns 5 rows. SQL Server creates a plan using an &lt;strong&gt;Index Seek&lt;/strong&gt;. Fast and lightweight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scenario B:&lt;/strong&gt; Later, another user passes a parameter that returns 500,000 rows. SQL Server reuses the cached "Index Seek" plan instead of doing an &lt;strong&gt;Index Scan&lt;/strong&gt;. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result? The server chokes trying to force a lightweight plan onto a massive dataset.&lt;/p&gt;




&lt;h3&gt;
  
  
  How to Catch Bad Plans in the Cache
&lt;/h3&gt;

&lt;p&gt;Instead of guessing or restarting the SQL Server service (which wipes the entire cache and hides the evidence!), you can inspect the plan cache to see what parameter values were used during compilation versus execution.&lt;/p&gt;

&lt;p&gt;Here’s a quick DMV snippet I use to find queries where the average execution duration is wildly higher than expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execution_count&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Exec_Count&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_elapsed_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execution_count&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Avg_Duration_ms&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_worker_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execution_count&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Avg_CPU_ms&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_logical_reads&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execution_count&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Avg_Logical_Reads&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="k"&gt;SUBSTRING&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statement_start_offset&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statement_end_offset&lt;/span&gt;
              &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;DATALENGTH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statement_end_offset&lt;/span&gt;
          &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statement_start_offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Query_Text&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;qp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_plan&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;XML_Execution_Plan&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dm_exec_query_stats&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dm_exec_sql_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sql_handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dm_exec_query_plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plan_handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;qp&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execution_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_elapsed_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execution_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;Clicking&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;XML_Execution_Plan&lt;/span&gt; &lt;span class="k"&gt;column&lt;/span&gt; &lt;span class="n"&gt;lets&lt;/span&gt; &lt;span class="n"&gt;you&lt;/span&gt; &lt;span class="k"&gt;view&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="n"&gt;graphical&lt;/span&gt; &lt;span class="n"&gt;execution&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="n"&gt;directly&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SSMS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for the Parameter List inside the properties window—it will show you the Compiled Value vs the Runtime Value. If they look drastically different, you've found your parameter sniffing culprit.&lt;/p&gt;

&lt;p&gt;How Do You Usually Fix Parameter Sniffing?&lt;br&gt;
There are several ways to tackle this depending on the SQL Server version and business context:&lt;/p&gt;

&lt;p&gt;Adding OPTIMIZE FOR UNKNOWN to the query.&lt;/p&gt;

&lt;p&gt;Using local variables inside stored procedures.&lt;/p&gt;

&lt;p&gt;Updating stale index statistics.&lt;/p&gt;

&lt;p&gt;Using Query Store (if enabled) to force a known good plan.&lt;/p&gt;

&lt;p&gt;How do you usually handle this in your production apps? Do you rely on query hints, or do you prefer fixing it at the database configuration level?&lt;/p&gt;

&lt;p&gt;Drop your thoughts in the comments!&lt;/p&gt;

</description>
      <category>sql</category>
      <category>database</category>
      <category>performance</category>
    </item>
    <item>
      <title>Why Do We Keep Forgetting About the Plan Cache in SQL Server?</title>
      <dc:creator>azhadsuhaimi</dc:creator>
      <pubDate>Fri, 07 Aug 2026 23:26:04 +0000</pubDate>
      <link>https://dev.to/azhadsuhaimi/why-do-we-keep-forgetting-about-the-plan-cache-in-sql-server-3l7e</link>
      <guid>https://dev.to/azhadsuhaimi/why-do-we-keep-forgetting-about-the-plan-cache-in-sql-server-3l7e</guid>
      <description>&lt;p&gt;Picture this: A user reports that the system is running painfully slow. &lt;/p&gt;

&lt;p&gt;What is your immediate knee-jerk reaction? &lt;/p&gt;

&lt;p&gt;For a long time, mine was to check table indexes, look at active locks, or blame the ORM for generating terrible SQL queries. But more often than not, the actual root cause was sitting right under my nose—inside SQL Server’s &lt;strong&gt;plan cache&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We talk a lot about optimizing queries during development, but we rarely talk about how much insight SQL Server passively collects for us in production while it's running.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Unsung Hero: &lt;code&gt;sys.dm_exec_query_stats&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;When SQL Server executes a query, it compiles an execution plan and caches it to save CPU time on subsequent runs. Along with that plan, it tracks execution metrics inside &lt;code&gt;sys.dm_exec_query_stats&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;It stores critical telemetry like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Execution Count:&lt;/strong&gt; Is a query slow because it’s inherently heavy, or because it’s being executed 50,000 times a minute (the classic N+1 problem)?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker Time:&lt;/strong&gt; How much raw CPU time (&lt;code&gt;total_worker_time&lt;/code&gt;) has this specific query consumed since the last service restart?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logical Reads:&lt;/strong&gt; Is the query thrashing memory and IO?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of firing up SQL Server Profiler or Extended Events (which can add performance overhead to a live server), pulling directly from the plan cache is practically free.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Tricky Part: Taming Large Batch Queries
&lt;/h3&gt;

&lt;p&gt;If you've ever queried &lt;code&gt;sys.dm_exec_query_stats&lt;/code&gt; and joined it with &lt;code&gt;sys.dm_exec_sql_text&lt;/code&gt;, you’ve probably hit a common pain point: &lt;strong&gt;It gives you the entire batch or stored procedure text.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a slow statement is hidden inside a 500-line stored procedure, looking at the entire text block doesn't immediately tell you &lt;em&gt;which exact query&lt;/em&gt; ate the CPU.&lt;/p&gt;

&lt;p&gt;To isolate the specific culprit, you have to do some offset math using &lt;code&gt;statement_start_offset&lt;/code&gt; and &lt;code&gt;statement_end_offset&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is a lightweight snippet I usually keep handy when I need to quickly inspect top CPU hogs without reading through endless blocks of SQL text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execution_count&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Execution_Count&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_worker_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Total_CPU_ms&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_worker_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execution_count&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Avg_CPU_ms&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_elapsed_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Total_Duration_ms&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_logical_reads&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Total_Logical_Reads&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;creation_time&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Plan_Cached_Since&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;DB_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dbid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Database_Name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;OBJECT_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objectid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dbid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Object_Name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="k"&gt;SUBSTRING&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statement_start_offset&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statement_end_offset&lt;/span&gt;
              &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;DATALENGTH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statement_end_offset&lt;/span&gt;
          &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statement_start_offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Query_Text&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dm_exec_query_stats&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dm_exec_sql_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sql_handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dm_exec_query_plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plan_handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;qp&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_worker_time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dividing total_worker_time by 1000 converts microseconds to readable milliseconds, and the SUBSTRING logic slices out the exact offending query statement.&lt;/p&gt;

&lt;p&gt;What's Your First Move During a Performance Issue?&lt;br&gt;
I’ve been working on organizing my own lightweight, read-only diagnostic scripts lately because I got tired of rewriting these DMV queries from memory during high-pressure troubleshooting.&lt;/p&gt;

&lt;p&gt;But I’m curious to know how other developers handle this:&lt;/p&gt;

&lt;p&gt;Do you regularly rely on Dynamic Management Views (DMVs) for quick health checks?&lt;/p&gt;

&lt;p&gt;Or do you prefer APM tools like Datadog, New Relic, or built-in Extended Events?&lt;/p&gt;

&lt;p&gt;Let’s discuss in the comments below!&lt;/p&gt;

</description>
      <category>sql</category>
      <category>database</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
