Solving the N+1 Query Problem with EF Core Eager Loading
Stop your application from crawling by eliminating N+1 queries. Learn how to use .Include and .ThenInclude to fix N+1 performance issues and avoid Cartesian Explosion.
22 Jul 2025, 01:21 UTC

The Silent Performance Killer: N+1 Queries
\nYou write a simple loop to display a list of orders and their associated customer names. On your local machine with ten records, it feels instantaneous. In production with ten thousand records, the application crawls. This is the classic N+1 query problem.
\nThe problem occurs when the application executes one query to fetch the main entities (the \"1\") and then executes a separate query for every single entity to fetch its related data (the \"N\"). If you have 100 orders, EF Core may send 101 separate requests to the database. This creates massive network latency and puts unnecessary load on the database server.
\nThe solution is Eager Loading. By using the .Include() method, you tell EF Core to fetch the related data as part of the initial query, typically using a SQL JOIN, reducing those 101 round-trips down to one.
Implementing Eager Loading
\nEager loading is handled via extension methods found in the Microsoft.EntityFrameworkCore namespace. The primary tool is .Include(), which targets a navigation property on your entity.
Loading Multi-Level Relationships
\nOften, you need data deeper than one level. For example, you might need an Order, the items within that order, and the specific Product details for each item. To achieve this, use .ThenInclude(). While .Include() starts from the root entity, .ThenInclude() chains off the previously included navigation property.
Filtered Include
\nLoading every related record can be wasteful. EF Core allows you to filter the related data directly within the .Include() statement. This ensures that only the necessary subset of related entities is transferred from the database to the application memory.
Worked Example: Order Management
\nConsider a scenario where we need to retrieve all \"Shipped\" orders, but we only want to include the OrderItems that cost more than $50, along with the Product details for those items.
\n\n// Run this within your Service or Repository layer\n// Required Permissions: Read access to the database context\npublic async Task<List<Order>> GetHighValueShippedOrdersAsync(AppDbContext context)\n{\n return await context.Orders\n .Where(o => o.Status == \"Shipped\")\n // Eager load OrderItems with a filter\n .Include(o => o.OrderItems.Where(item => item.Price > 50))\n // Chain to load the Product for each filtered item\n .ThenInclude(item => item.Product)\n .ToListAsync();\n}\n\nExpected Result: EF Core generates a SQL query using JOINs (or split queries) that retrieves the Orders, the filtered OrderItems, and the Products in a highly optimized manner, regardless of how many orders are returned.
\n\nThe Trade-off: Cartesian Explosion
\nEager loading is not a silver bullet. When you include multiple collection navigations (e.g., .Include(o => o.Items).Include(o => o.Notes)), the database generates a Cartesian Product. This means the result set multiplies the rows of one collection by the rows of the other, leading to a massive amount of redundant data being sent over the wire.
This is known as Cartesian Explosion. It can lead to high memory usage on the application server and slower query execution.
\n\nMitigation: AsSplitQuery()
\nTo avoid this, EF Core provides .AsSplitQuery(). This tells EF Core to execute multiple separate queries (one for each collection) and join them in the application memory, rather than one giant JOIN in SQL.
var orders = await context.Orders\n .Include(o => o.OrderItems)\n .Include(o => o.ShippingHistory)\n .AsSplitQuery()\n .ToListAsync();\n\nVerification and Diagnostics
\nTo verify that you have successfully eliminated N+1 queries, you must inspect the generated SQL. You can do this by enabling sensitive data logging in your DbContext configuration during development:
optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);\noptionsBuilder.EnableSensitiveDataLogging();\n\nCheck your debug console. If you see a single SELECT statement with JOINs, eager loading is working. If you see a stream of SELECT statements firing rapidly as you iterate through your results, you are still experiencing the N+1 problem.
Summary Checklist
\n- \n
- Use
.Include()for direct relationships. \n - Use
.ThenInclude()for nested relationships. \n - Apply filters inside
.Include()to reduce data transfer. \n - Use
.AsSplitQuery()when loading multiple collections to prevent Cartesian Explosion. \n - Verify the SQL output to ensure the number of round-trips is minimized. \n
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.