EF Core N+1 Query Problem: How to Detect, Measure, and Fix

If your EF Core application works perfectly with a small dataset but suddenly slows down as the amount of data grows, don’t immediately blame your API, server, or database hardware.

The problem may be much simpler: your application is making too many database queries.

One of the most common causes is the N+1 query problem in EF Core.

The pattern is straightforward: the application executes one query to retrieve a collection of records and then makes additional queries to retrieve related data for each record.

For example, suppose an application loads 100 invoices and then retrieves customer information separately for each invoice. The result could be:

1 initial query + 100 additional queries = 101 database calls

The exact number depends on the number of records and the application’s data-access pattern. The underlying problem is the same: too many database round trips for a single business operation.

What’s tricky is that the C# code can look perfectly reasonable while the generated SQL tells a very different story.

In this article, we’ll look at what causes N+1 queries in EF Core, how to detect and measure them, when to use Include(), Select(), or explicit loading, and why fewer queries isn’t always the same as better performance.

What Is the N+1 Query Problem in EF Core?

The N+1 problem happens when an application executes:

  • One query to retrieve a collection of records
  • N additional queries to retrieve related data for each record

That’s where the name comes from:

1 + N = N+1 database queries

Consider a simple invoice and customer relationship:

public class Invoice
{
    public int Id { get; set; }
    public decimal Amount { get; set; }

    public int CustomerId { get; set; }
    public Customer Customer { get; set; }
}

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Now imagine loading invoices:

var invoices = await dbContext.Invoices.ToListAsync();

The application may execute one query to retrieve the invoices.

The problem can appear later when related data is accessed repeatedly:

foreach (var invoice in invoices)
{
    Console.WriteLine(invoice.Customer.Name);
}

If lazy loading is enabled and configured, accessing invoice.Customer can trigger an additional database query for each invoice.

For 100 invoices, that could result in:

1 query for invoices
+
100 queries for customers
=
101 database calls

The important point is that EF Core does not automatically create an N+1 problem simply because navigation properties exist. The additional queries depend on the application’s data-loading strategy and how related data is accessed.

Why Is N+1 a Problem?

At first, an extra database query might not seem significant.

If you have five records, five additional queries may not create an obvious performance problem.

But applications rarely stay at five records.

As data volume, concurrent users, and request frequency increase, the cost of those additional database round trips can become significant.

1. Higher API Latency

Every database round trip introduces overhead.

When a request results in dozens or hundreds of database calls, the cumulative latency can become much larger than expected.

An API that responds quickly with 10 records may behave very differently with 1,000 records.

2. Increased Database Workload

The database has to parse, execute, and return results for every query.

Even if each individual query is fast, hundreds or thousands of repeated queries can create unnecessary database workload.

3. Higher Infrastructure Costs

More database activity can mean:

  • Higher CPU utilization
  • Increased connection pressure
  • More database capacity
  • Additional cloud resources
  • Higher infrastructure costs

This becomes particularly important for applications running on cloud infrastructure where database resources scale with workload.

4. Scaling Problems

N+1 issues often become more visible as an application grows.

For example:

Small dataset
10 records
↓
11 database calls

Larger dataset
500 records
↓
501 database calls

Production workload
5,000 records
↓
5,001 database calls

The application may appear perfectly healthy during development while struggling under production-scale data.

5. SLA and User Experience Risk

Slow database access eventually becomes slow application behavior.

For customer-facing systems, this can affect:

  • API response times
  • Page load performance
  • Background jobs
  • Batch processing
  • User experience
  • SLA/SLO targets

That’s why N+1 isn’t simply a coding-style issue. It can become a production performance problem.

Why N+1 Problems Often Go Unnoticed

One of the biggest reasons N+1 problems survive development is that the application can appear to work correctly.

Suppose a developer tests an invoice page with 10 records.

Everything looks fine.

The same application might behave very differently when production contains:

  • 10,000 invoices
  • Thousands of customers
  • Multiple concurrent users
  • Large reporting workloads
  • Background processes running simultaneously

Functional testing answers an important question:

Does the application return the correct result?

Performance testing needs to answer a different question:

How much database work does the application perform to produce that result?

Those are not the same thing.

Does EF Core Always Cause N+1 Queries?

No.

EF Core does not automatically generate an N+1 query every time you access a navigation property.

N+1 behavior typically appears because of the application’s data-loading pattern.

Common causes include:

  • Lazy loading
  • Accessing navigation properties inside loops
  • Loading related entities individually
  • Repeated repository calls
  • Data-access logic hidden inside helper methods
  • Queries that retrieve incomplete data and then fetch more data repeatedly

For example:

var invoices = await dbContext.Invoices.ToListAsync();

foreach (var invoice in invoices)
{
    var customerName = invoice.Customer.Name;
}

This pattern becomes problematic when Customer is lazy-loaded.

The C# code looks simple, but the database activity may look more like:

SELECT ... FROM Invoices

SELECT ... FROM Customers WHERE Id = 1
SELECT ... FROM Customers WHERE Id = 2
SELECT ... FROM Customers WHERE Id = 3
...

The database sees many separate operations even though the application code appears to perform one simple business operation.

How to Detect N+1 Queries in EF Core

The first step toward fixing N+1 is finding out what your application is actually sending to the database.

Don’t guess.

Measure it.

1. Enable EF Core SQL Logging

EF Core can log generated SQL queries.

For example:

builder.Services.AddDbContext<AppDbContext>(options =>
{
    options
        .UseSqlServer(connectionString)
        .LogTo(Console.WriteLine, LogLevel.Information);
});

When the application runs, you can inspect the SQL being generated.

If one request produces:

SELECT ... FROM Invoices

SELECT ... FROM Customers WHERE Id = 1
SELECT ... FROM Customers WHERE Id = 2
SELECT ... FROM Customers WHERE Id = 3
SELECT ... FROM Customers WHERE Id = 4
...

you have a strong indication that related data is being loaded repeatedly.

2. Count Database Queries

Another useful technique is to measure how many database queries are executed for a particular operation.

For example, you might establish an expectation such as:

Invoice list request
Expected database queries: 1–3
Actual database queries: 101

The difference immediately highlights a potential problem.

Query-count monitoring can be particularly useful in integration or performance tests.

3. Inspect Generated SQL

Don’t only count queries.

Look at the SQL itself.

Two implementations might both execute one query while producing very different SQL and returning very different amounts of data.

This is why database performance analysis should consider:

  • Number of queries
  • Query execution time
  • Rows returned
  • Columns returned
  • Joins
  • Index usage
  • Database CPU
  • Memory consumption

The goal isn’t simply to reduce a number.

The goal is to understand the work being performed.

4. Test With Realistic Data Volumes

A query pattern that looks harmless with 20 records can become expensive with 20,000.

Performance testing should use data volumes that resemble realistic production workloads.

Also test with realistic concurrency.

A query that takes 5 ms for one request may become a much bigger problem when hundreds of users execute it simultaneously.

How to Fix the N+1 Query Problem

There isn’t one universal solution.

The correct approach depends on the data your application actually needs.

Three common strategies are:

  1. Include()
  2. Select() projection
  3. Explicit loading

Let’s look at each.

1. Use Include() When You Need Related Entities

If you genuinely need the related entity, eager loading with Include() can avoid the per-record loading pattern.

Instead of:

var invoices = await dbContext.Invoices.ToListAsync();

foreach (var invoice in invoices)
{
    Console.WriteLine(invoice.Customer.Name);
}

you can load the related customer data as part of the query operation:

var invoices = await dbContext.Invoices
    .Include(i => i.Customer)
    .ToListAsync();

Now EF Core knows that the related customer data is required.

The exact SQL shape depends on the query and EF Core configuration, but the important point is that you’re explicitly defining the required relationship instead of relying on repeated lazy loading.

When Include() Makes Sense

Use Include() when:

  • You actually need the related entity
  • The object graph is reasonably sized
  • You need entity tracking
  • The relationship is part of the business operation

However, Include() isn’t automatically the best solution for every performance problem.

2. Use Select() When You Only Need Specific Fields

Sometimes you don’t need the entire entity.

Suppose an API only needs:

  • Invoice ID
  • Invoice amount
  • Customer name

Loading the entire invoice and customer entities may retrieve much more data than necessary.

Instead, project directly into the shape the API needs:

var invoices = await dbContext.Invoices
    .Select(i => new
    {
        i.Id,
        i.Amount,
        CustomerName = i.Customer.Name
    })
    .ToListAsync();

This approach can allow EF Core to generate SQL that retrieves only the required columns.

Projection is often useful for:

  • APIs
  • Reporting
  • Dashboards
  • Read-only screens
  • Large datasets
  • Performance-sensitive queries

Instead of asking:

How can I load this entire object graph?

ask:

What data does this operation actually need?

That question often leads to a more efficient query.

3. Use Explicit Loading When You Need More Control

Explicit loading gives you control over when related data is loaded.

For example:

var invoice = await dbContext.Invoices
    .FirstAsync(i => i.Id == invoiceId);

await dbContext.Entry(invoice)
    .Reference(i => i.Customer)
    .LoadAsync();

This can be useful when related data is optional or should only be retrieved under certain conditions.

The trade-off is additional code and more responsibility for managing when queries are executed.

Include vs. Select vs. Explicit Loading

Here’s a simplified comparison:

Approach Best For Benefits Considerations
Include() Loading related entities Simple and readable Can load more data than necessary
Select() Read-only data and APIs Fetches only required fields Requires defining the projection
Explicit Loading Conditional related data More control More verbose and can still create extra queries

There isn’t a universal winner.

The right choice depends on:

  • Data volume
  • Object graph size
  • Query complexity
  • Tracking requirements
  • API response shape
  • Performance requirements

Fewer Queries Isn’t Always Better

This is an important point.

The goal isn’t:

Make the query count as small as possible.

The goal is:

Make the database work appropriate for the operation.

For example, imagine an application loads a huge object graph using one complicated query.

It may technically execute one database query, but that query could:

  • Return thousands of rows
  • Include unnecessary columns
  • Contain many joins
  • Produce duplicated data
  • Consume significant memory
  • Take longer to execute

In that situation:

1 query does not automatically mean better performance.

This is why performance optimization needs to consider more than query count.

A better approach is to measure:

Query count
+
Execution time
+
Rows returned
+
Data transferred
+
Database resource usage
+
Application memory

The best solution is usually the one that provides predictable performance for the actual workload.

A Practical Example

Consider an API that returns 100 invoices and the name of each customer.

A problematic pattern might look like:

var invoices = await dbContext.Invoices.ToListAsync();

foreach (var invoice in invoices)
{
    Console.WriteLine(invoice.Customer.Name);
}

With lazy loading configured, the application could generate:

1 invoice query
+
100 customer queries
=
101 database calls

A more appropriate approach could be projection:

var invoices = await dbContext.Invoices
    .Select(i => new
    {
        i.Id,
        i.Amount,
        CustomerName = i.Customer.Name
    })
    .ToListAsync();

Now the application describes the exact data required by the operation.

The important improvement isn’t simply changing 101 queries to 1 query.

The improvement is that the application has a more deliberate data-access strategy.

How to Prevent N+1 Problems in Production

Fixing N+1 issues after they reach production is much more expensive than preventing them during development.

Here are some practical checks.

1. Review Generated SQL

For performance-sensitive queries, inspect the SQL generated by EF Core.

Don’t assume the LINQ code tells the whole story.

2. Watch for Navigation Properties Inside Loops

Code like this should trigger a review:

foreach (var item in items)
{
    var relatedData = item.RelatedEntity;
}

Ask whether accessing the navigation property can trigger another database query.

3. Measure Query Counts

For important operations, establish reasonable query-count expectations.

For example:

Dashboard request
Expected: 1–5 queries
Actual: 150 queries

That difference deserves investigation.

4. Use Production-Like Data

Don’t test only with five records.

Use realistic:

  • Data volume
  • Relationships
  • Query complexity
  • Concurrent requests

Performance problems often become visible only when the workload resembles production.

5. Review Performance During Code Reviews

Database performance should be part of normal code review.

When reviewing EF Core code, ask:

  • What SQL will this generate?
  • How many database round trips can this produce?
  • Are navigation properties being accessed repeatedly?
  • Do we need the entire entity?
  • Would projection be better?
  • What happens when the dataset grows 10x?

These questions can catch problems before deployment.

A Simple N+1 Prevention Checklist

Before shipping an EF Core feature, ask:

  • Have I checked the generated SQL?
  • Could this navigation property trigger additional queries?
  • Am I accessing related data inside a loop?
  • Do I actually need the full related entity?
  • Would Select() projection be more appropriate?
  • Is Include() loading more data than necessary?
  • Have I measured database query counts?
  • Have I tested with realistic data volumes?
  • Have I tested under realistic concurrency?
  • Have I considered database execution time, not just query count?

These checks don’t eliminate every database performance problem, but they make N+1 issues much easier to detect before they become production incidents.

Final Thoughts

The N+1 query problem is easy to underestimate because the application can work perfectly during development.

The code may look clean.

The tests may pass.

The API may respond quickly.

Then the dataset grows, more users arrive, and suddenly the database is handling hundreds or thousands of unnecessary requests.

That’s why EF Core performance should be measured at the database level, not judged only by how simple the C# code looks.

Use Include() when you genuinely need related entities. Use Select() when you only need specific fields. Use explicit loading when you need more control.

Most importantly, measure the actual database behavior.

The goal isn’t simply fewer queries.

The goal is predictable performance, controlled database workload, and appropriate data access as your application scales.

Best Entity Framework 7 Hosting

Fortunately, there are a number of dependable and recommended web hosts available that can help you gain control of your website’s performance and improve your Entity Framework 7 web ranking. HostForLIFE.eu is highly recommended. In Europe, HostForLIFE.eu is the most popular option for first-time web hosts searching for an affordable plan. Their standard price begins at only €3.49 per month. Customers are permitted to choose quarterly and annual plans based on their preferences. HostForLIFE.eu guarantees “No Hidden Fees” and an industry-leading ’30 Days Cash Back’ policy. Customers who terminate their service within the first thirty days are eligible for a full refund.

By providing reseller hosting accounts, HostForLIFE.eu also gives its consumers the chance to generate income. You can purchase their reseller hosting account, host an unlimited number of websites on it, and even sell some of your hosting space to others. This is one of the most effective methods for making money online. They will take care of all your customers’ hosting needs, so you do not need to fret about hosting-related matters.