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:
Include()Select()projectionExplicit 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 |
|---|---|---|---|
| Loading related entities | Simple and readable | Can load more data than necessary |
| 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.

Join the conversation! Your thoughts help the community grow.