Data layer and SQL
The N+1 problem, Include and AsNoTracking
The hidden cost of querying in a loop, and making read queries lighter.
Understand the concept
The N+1 problem is running one query for a list and then one more query for each item in it. If each post’s author is loaded separately while listing 20 posts, 1 + 20 = 21 queries run. Each looks fast alone, but the network round trips add up, the page slows and the problem grows with data. It hides in development with little data and surfaces in production.
The fix is fetching related data in the same query. Include loads a navigation property with a JOIN. For reads, projection is usually better: using p.Author.Name inside Select lets EF Core emit a JOIN with only the needed columns.
By default EF Core tracks loaded entities for changes. For read-only data that wastes memory and CPU. AsNoTracking turns tracking off and is faster and lighter for read endpoints. The simulator’s ‘Slow list: N+1’ scenario shows the difference on the timing waterfall.
N+1 is the most common ORM performance problem in production and easily missed when reading code.
Enabling lazy loading and touching navigation properties inside a loop.
A letter’s journey through the city
Think of a request as a letter and the server as a city. Pick a scenario, press play and watch which gates the letter passes and where it gets stopped.
The blog page asks for 20 posts with their authors’ names.
One topic. Three levels of depth.
// ✗ N+1
var posts = await db.Posts.Take(20).ToListAsync(ct);
foreach (var p in posts)
p.AuthorName = (await db.Authors.FindAsync(p.AuthorId))!.Name; // 20 ek sorguEvery await in the loop is a database round trip.
This page connects to no real server or database. Code samples are illustrative for .NET 8, Next.js and SQL Server; timings are example values in the simulation, not measurements. Try samples only in your own development environment; never share real connection strings, tokens or passwords.
Try it yourself first.
An orders page shows each order’s customer and items. For 50 orders the SQL log shows 101 queries. Which two relationships cause N+1, and how do you get to one query?
Show the solution and explanation
1 (siparişler) + 50 (müşteri) + 50 (ürünler) = 101
Düzeltme: projeksiyon
db.Orders.AsNoTracking().Select(o => new OrderView(
o.Id, o.Customer.Name,
o.Items.Select(i => i.Product.Name).ToList()))
// koleksiyon için AsSplitQuery() de değerlendirilebilirLoading each relationship in a loop adds N queries. Projection turns relationships into JOINs; for large collections, split queries avoid cartesian explosion.