Introduction
Entity Framework Core (EF Core) is one of the most popular Object-Relational Mappers (ORMs) for .NET applications. It simplifies database access by allowing developers to work with C# objects instead of writing SQL queries for every operation. While EF Core improves productivity, poorly optimized queries can lead to slow application performance, excessive memory usage, and unnecessary database load.
Whether you're building an ASP.NET Core Web API, a Blazor application, or a desktop application, understanding how to optimize EF Core queries is essential for creating fast and scalable software.
In this article, you'll learn practical EF Core performance tips that help reduce query execution time, improve database efficiency, and enhance the overall performance of your applications.
Why EF Core Performance Matters
Every database query consumes resources. As your application grows, inefficient queries can cause:
Slow page loading
High database CPU usage
Increased memory consumption
Longer API response times
Poor user experience
Reduced application scalability
Optimizing EF Core queries helps your application handle more users while reducing infrastructure costs.
Select Only the Required Columns
One of the most common performance mistakes is retrieving entire entities when only a few fields are needed.
Instead of this:
var products = await context.Products.ToListAsync();
Select only the required columns.
var products = await context.Products
.Select(p => new
{
p.Id,
p.Name,
p.Price
})
.ToListAsync();
Fetching fewer columns reduces:
Network traffic
Memory usage
Query execution time
This is especially beneficial for large tables.
Use AsNoTracking() for Read-Only Queries
By default, EF Core tracks every entity it retrieves. Change tracking is useful for updates but unnecessary for read-only operations.
For queries that don't modify data, use AsNoTracking().
var products = await context.Products
.AsNoTracking()
.ToListAsync();
Benefits include:
Lower memory usage
Faster query execution
Reduced CPU overhead
Use this approach whenever you're displaying data without updating it.
Filter Data at the Database Level
Avoid loading unnecessary records into memory.
Instead of retrieving all records and filtering afterward:
var products = await context.Products.ToListAsync();
var expensiveProducts = products.Where(p => p.Price > 1000);
Filter directly in the database.
var expensiveProducts = await context.Products
.Where(p => p.Price > 1000)
.ToListAsync();
Database servers are optimized for filtering data efficiently.
Use Pagination
Loading thousands of records at once can significantly affect performance.
Instead, retrieve data in smaller batches.
var products = await context.Products
.OrderBy(p => p.Id)
.Skip(20)
.Take(10)
.ToListAsync();
Pagination helps:
Improve response times
Reduce memory usage
Enhance user experience
Support scalable APIs
It's an essential practice for applications that display large datasets.
Avoid the N+1 Query Problem
The N+1 query problem occurs when EF Core executes one query to retrieve parent records and then additional queries for each related entity.
For example:
var orders = await context.Orders.ToListAsync();
foreach (var order in orders)
{
Console.WriteLine(order.Customer.Name);
}
If lazy loading is enabled, this can generate many unnecessary database queries.
Use eager loading instead.
var orders = await context.Orders
.Include(o => o.Customer)
.ToListAsync();
This retrieves all required data in fewer database calls.
Use Split Queries for Large Relationships
When loading multiple related collections, a single query may produce large result sets due to multiple joins.
EF Core supports split queries.
var orders = await context.Orders
.Include(o => o.Items)
.AsSplitQuery()
.ToListAsync();
Split queries can reduce duplicate data transfer and improve performance in complex object graphs.
Use Compiled Queries
If the same query is executed frequently, compiling it once can reduce processing overhead.
Example:
private static readonly Func<AppDbContext, int, Task<Product?>> GetProduct =
EF.CompileAsyncQuery(
(AppDbContext context, int id) =>
context.Products.FirstOrDefault(p => p.Id == id));
Compiled queries are particularly useful in high-traffic applications where the same query runs repeatedly.
Create Proper Database Indexes
Even well-written EF Core queries depend on efficient database indexes.
For example, if you frequently search by email:
var customer = await context.Customers
.FirstOrDefaultAsync(c => c.Email == email);
Creating an index on the Email column can significantly improve query performance.
Review your application's most common search conditions and ensure appropriate indexes exist.
Use Async Database Operations
Asynchronous queries prevent application threads from being blocked while waiting for the database.
Instead of synchronous operations:
var products = context.Products.ToList();
Use asynchronous methods.
var products = await context.Products.ToListAsync();
Common asynchronous methods include:
ToListAsync()
FirstOrDefaultAsync()
SingleAsync()
CountAsync()
SaveChangesAsync()
Async operations improve scalability, especially in web applications handling multiple concurrent requests.
Avoid Unnecessary Include Statements
Including related entities that aren't used increases query complexity and data transfer.
Instead of loading every related table, include only the data required for the current operation.
Review your queries regularly to remove unnecessary Include() statements.
Monitor Generated SQL
EF Core translates LINQ queries into SQL. Sometimes the generated SQL may not be as efficient as expected.
You can inspect the generated SQL using:
var sql = context.Products
.Where(p => p.Price > 500)
.ToQueryString();
Console.WriteLine(sql);
Reviewing generated SQL helps identify inefficient joins, filters, or unnecessary data retrieval.
Best Practices
Follow these recommendations to improve EF Core performance:
Select only the columns you need.
Use AsNoTracking() for read-only queries.
Apply filtering in the database.
Implement pagination for large datasets.
Avoid the N+1 query problem.
Use eager loading only when necessary.
Consider split queries for complex relationships.
Create indexes for frequently queried columns.
Use asynchronous database operations.
Monitor generated SQL and optimize slow queries.
Applying these practices consistently can lead to noticeable performance improvements.
Common Performance Mistakes
Developers often encounter performance issues because of avoidable mistakes.
Some common examples include:
Loading entire tables into memory.
Retrieving unnecessary columns.
Using synchronous database operations in web applications.
Overusing Include() statements.
Ignoring database indexes.
Executing repeated queries inside loops.
Failing to analyze generated SQL.
Identifying and correcting these issues can dramatically improve application responsiveness.
Conclusion
Entity Framework Core provides a powerful and productive way to work with databases, but achieving optimal performance requires thoughtful query design. By selecting only the required data, using AsNoTracking() for read-only scenarios, implementing pagination, avoiding the N+1 query problem, leveraging compiled queries, and monitoring generated SQL, you can build applications that are both efficient and scalable.
Performance optimization is an ongoing process. Regularly reviewing database queries, measuring execution times, and applying EF Core best practices will help ensure your applications continue to deliver fast and reliable experiences as they grow.