πŸ“– 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 πŸ”₯)



Project Source Code:

Download
Complete and Continue  
Discussion

4 comments