π Loading Related Data (Include) + N+1 Problem π₯
π Loading Related Data (Include) + N+1 Problem π₯
ποΈ Introduction
In previous lessons, you learned:
β filtering (Where)
β projection (Select)
β aggregation (Count, Sum, Average)
Now we answer a new question:
π How do we load related data (relationships)?
Example:
π Students β Enrollments
π Courses β Instructor
π Core Idea
π Navigation properties are NOT loaded automatically
π If you donβt explicitly load related data:
β EF Core will NOT include it
π― Key Teaching Point (VERY IMPORTANT)
π If you access related data without loading it correctly, you may create the N+1 problem
π§ Mental Model
Entity β Related Data Β Β β Include(...) Β Β β EF builds JOIN Β Β β ONE SQL query
π» Full Console Example (BAD vs GOOD)
/*
========================================================
Code Overview
--------------------------------------------------------
Purpose:
- Demonstrate loading related data
- Show N+1 problem using a bad approach
- Show Include() solution using a good approach
- Compare SQL query shapes before execution
Key Points:
- BAD = multiple queries, known as the N+1 problem
- GOOD = related data loaded using Include()
- Include() can reduce repeated database calls
- ToQueryString() previews SQL query shape
- Runtime logging shows actual executed SQL
========================================================
*/
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using TrainingCenter.Data;
// Configuration setup
IConfiguration configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.Build();
// Read connection string
string? connectionString =
configuration.GetConnectionString("DefaultConnection");
// Validate values
if (string.IsNullOrWhiteSpace(connectionString))
{
Console.WriteLine("Connection string not found.");
return;
}
// Create options
var options =
new DbContextOptionsBuilder<AppDbContext>()
.UseSqlServer(connectionString)
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging()
.Options;
// Create context
using var context = new AppDbContext(options);
// Test connection if relevant
if (!context.Database.CanConnect())
{
Console.WriteLine("Could not connect to TrainingCenterDB.");
return;
}
Console.WriteLine("Connected successfully.");
Console.WriteLine();
// Call main methods
CompareInclude(context);
/// <summary>
/// Demonstrates bad vs good loading of related data using manual queries vs Include().
/// </summary>
static void CompareInclude(AppDbContext context)
{
ShowBadNPlusOneApproach(context);
PrintSeparator();
ShowGoodIncludeApproach(context);
}
/// <summary>
/// Demonstrates the bad N+1 approach by loading students first,
/// then running one additional count query per student.
/// </summary>
static void ShowBadNPlusOneApproach(AppDbContext context)
{
Console.WriteLine("BAD APPROACH - N+1 Problem");
Console.WriteLine("--------------------------");
Console.WriteLine();
// Build query first
var studentsQuery =
context.Students;
// Preview SQL before execution
PreviewSQLUsingToQueryString(studentsQuery.ToQueryString());
// Execute first query: load all students
var students = studentsQuery.ToList();
Console.WriteLine();
foreach (var student in students)
{
// Build query first for each student
var enrollmentsQuery =
context.Enrollments
.Where(e => e.StudentId == student.StudentId);
// Preview SQL query shape
PreviewSQLUsingToQueryString(enrollmentsQuery.ToQueryString());
// Execute Count() for each student
// ToQueryString previews query shape,
// runtime logging shows actual executed SQL for Count().
int enrollmentsCount =
enrollmentsQuery.Count();
Console.WriteLine(
$"{student.FirstName} {student.LastName} - Enrollments: {enrollmentsCount}");
}
Console.WriteLine();
Console.WriteLine("Problem: One query for students + one query per student.");
Console.WriteLine();
}
/// <summary>
/// Demonstrates the good approach by loading students with their enrollments using Include().
/// </summary>
static void ShowGoodIncludeApproach(AppDbContext context)
{
Console.WriteLine("GOOD APPROACH - Include()");
Console.WriteLine("-------------------------");
Console.WriteLine();
// Build query first
var query =
context.Students
.Include(s => s.Enrollments);
// Preview SQL before execution
PreviewSQLUsingToQueryString(query.ToQueryString());
// Execute query
var studentsWithEnrollments = query.ToList();
Console.WriteLine();
foreach (var student in studentsWithEnrollments)
{
Console.WriteLine(
$"{student.FirstName} {student.LastName} - Enrollments: {student.Enrollments.Count}");
}
Console.WriteLine();
Console.WriteLine("Result: Related enrollments are loaded with the students.");
Console.WriteLine();
}
/// <summary>
/// Prints a separator between examples.
/// </summary>
static void PrintSeparator()
{
Console.WriteLine(new string('-', 60));
Console.WriteLine();
}
/// <summary>
/// Displays generated SQL before execution.
/// </summary>
static void PreviewSQLUsingToQueryString(string SQLString)
{
Console.WriteLine("\nPreview SQL using ToQueryString():");
Console.WriteLine("----------------------------------");
Console.WriteLine(SQLString);
Console.WriteLine();
}
π SQL Comparison
β BAD (N+1 Problem)
Query 1
SELECT * FROM Students;
Then for EACH student:
SELECT COUNT(*) FROM Enrollments WHERE StudentId = @StudentId;
π If you have 100 students:
β 101 queries executed
β GOOD (Include)
SELECT [s].[StudentId], [s].[FirstName], ...,Β Β Β Β Β [e].[EnrollmentId], [e].[StudentId], ... FROM [Students] AS [s] LEFT JOIN [Enrollments] AS [e] ON [s].[StudentId] = [e].[StudentId];
π Result:
β ONE query only not 101 queries !!!
β οΈ The N+1 Problem (VERY IMPORTANT π₯)
β What is it?
1 query β load main data N queries β load related data
β Example
Students = 100 1 query β students 100 queries β enrollments Total = 101 queries β
β Correct Approach
1 query β everything
π‘ Real-World Scenario
π― Dashboard
π Show:
β Student Name
β Number of Courses
β Bad
foreach (var student in students)
{
Β Β context.Enrollments.Count(...)
}
β Good
context.Students Β Β .Include(s => s.Enrollments)
πΌ Professional Insight
In real-world systems:
π N+1 problem is one of the
most common performance issues
π It causes:
β slow applications
β database overload
β unnecessary queries
π Professionals ALWAYS:
β use Include()
β or projection
β or optimized queries
π§ Mental Anchor (Lock This π)
π If you see a loop with a database call inside it β you likely have an N+1 problem.
π Interconnection
Now you understand:
β how to load related data
β what Include() does
β how JOIN is generated
β how to avoid N+1 problem
π Next step:
π Projection vs Include (Which one to use? π₯)
π Conclusion
In this lesson, you learned:
β how to load related data using Include()
β how N+1 problem happens
β how to compare bad vs good approaches
β how EF Core generates JOIN queries
π This is one of the most critical performance lessons in EF Core π
π Next Lesson
π Projection vs Include (When to Use Each π₯)
4 comments