Home / Blog / Entity Framework Core Performance Tuning: N+1 Queries, Eager Loading, and Pagination (2026)
Database

Entity Framework Core Performance Tuning: N+1 Queries, Eager Loading, and Pagination (2026)

AJAjish Stephen August 11, 2026 10 min read
Entity Framework Core Performance Tuning: N+1 Queries, Eager Loading, and Pagination (2026)

Entity Framework Core makes database interactions feel natural in C#, but that convenience hides real performance costs. The most damaging bugs are silent: queries that execute hundreds of times when one would suffice. This guide covers the patterns that actually perform in production.

The N+1 Problem and Eager Loading

This query fires one query for orders, then one query per order to fetch customers—dangerous at scale:

// BAD: N+1 queries
varorders=await_db.Orders.ToListAsync();
foreach(varorderinorders)
{
    varcustomer=order.Customer.Name;// Lazy load fires here
}

Instead, eager-load the related data:

// GOOD: Single query with JOIN
varorders=await_db.Orders
    .Include(o=>o.Customer)
    .ToListAsync();

For complex relationships, use Select projection to fetch only what you need:

// BEST: Only load required columns
varorders=await_db.Orders
    .Select(o=>newOrderDto
    {
        Id=o.Id,
        Total=o.Total,
        CustomerName=o.Customer.Name
    })
    .ToListAsync();

Pagination Without Loading Everything

Skip/Take generates SQL's OFFSET/LIMIT, keeping memory usage constant regardless of total rows:

varpageSize=50;
varpage=1;

varorders=await_db.Orders
    .OrderByDescending(o=>o.CreatedAt)
    .Skip((page-1)*pageSize)
    .Take(pageSize)
    .ToListAsync();

Batch Operations for Bulk Updates

EF Core translates this into 1000 separate UPDATE statements—a nightmare:

// BAD: 1000 UPDATE queries
varitems=await_db.Items.ToListAsync();
foreach(variteminitems)
{
    item.Status="archived";
}
await_db.SaveChangesAsync();

Instead, batch the update:

// GOOD: Single UPDATE statement
await_db.Items
    .Where(i=>i.CreatedBefore<DateTime.UtcNow.AddYears(-1))
    .ExecuteUpdateAsync(s=>s.SetProperty(i=>i.Status,"archived"));

Disable Tracking for Read-Only Queries

EF Core tracks every entity returned so it can detect changes on SaveChanges(). For read-only queries, this is wasted memory and CPU:

varorders=await_db.Orders
    .AsNoTracking()// Tells EF not to track changes
    .ToListAsync();

Optimizing database queries is half the battle—the other half is knowing which patterns scale. This is the kind of architecture work I help teams tackle as part of custom software development engagements where performance matters.

Common questions

Why is lazy loading dangerous in EF Core?
Lazy loading executes a database query whenever you access a related entity that wasn't explicitly loaded. In a loop, this causes N+1 queries—one for the parent, then one for each child. Use Include() or Select projection instead.
Should I disable lazy loading entirely?
Many teams do, to force explicit eager loading and catch N+1 bugs during development. You can set LazyLoadingEnabled = false in OnConfiguring(). This makes performance issues visible immediately rather than as surprises in production.
Is AsNoTracking a silver bullet?
For read-only queries, yes—it reduces memory and CPU overhead significantly. But don't use it on entities you plan to modify; EF Core can't track changes on untracked entities, so SaveChanges() won't persist your edits.
What's the difference between Skip/Take and LIMIT/OFFSET?
They're the same thing. Skip(n).Take(m) generates SQL's OFFSET n ROWS FETCH NEXT m ROWS ONLY. It's database-efficient and keeps your query result sets bounded regardless of total data size.
Struggling with slow database queries?
I help teams identify and eliminate performance bottlenecks in their EF Core applications.
Explore Custom Software Development →
© Copyright 2024 Ajish Stephen