# Queries מתקדמות — Include ובעיית ה-N+1📖 שיעור

## מה זה?

`context.Students.ToList()` שולף את כל התלמידים — אבל בלי הציונים שלהם (`List<Grade> Grades`, ה-Navigation Property שהגדרנו שני שיעורים אחורה), למרות שהמחלקה `Student` מכילה אותו. למה? EF Core, כברירת מחדל, **לא** טוען קשרים אוטומטית — צריך לבקש אותם במפורש עם `Include`. אם שוכחים את זה ומנסים לגשת ל-`student.Grades` בלולאה על כל תלמיד, נופלים בדיוק לתוך בעיית ה-N+1 — אחת ממלכודות הביצועים הנפוצות ביותר בעבודה עם ORM.

השיעור הזה שונה מקודמיו בדבר אחד: את הבעיה (N+1) לא רק **נסביר** — נראה אותה **בעיניים**, ממש בקונסולה שלכם, כי EF Core כבר מדפיס לשם כל שאילתת SQL שהוא שולח בזמן פיתוח, בלי שום הגדרה נוספת מצדכם.

## מילות מפתח שחשוב לזכור

• LINQ על `DbSet` — אותו `Where`/`Select`/`OrderBy` מיחידת ה-LINQ, אך EF Core מתרגם אותם ל-SQL אמיתי מאחורי הקלעים

• `Include` — מבקש מ-EF Core לטעון Navigation Property מסוימת יחד עם השאילתה הראשית (JOIN אחד, לא שאילתות נפרדות)

• N+1 Problem — שאילתה אחת לרשימה ראשית + שאילתה **נפרדת לכל איבר** בה — במקום JOIN יחיד, נשלחות N+1 שאילתות ל-DB

• Deferred Execution — שאילתת LINQ על `DbSet` לא רצה מיד; היא רצה רק כשקוראים `.ToList()`/`.First()` וכו'

## בונים הבנה שלב אחר שלב — ומוכיחים כל שלב בקונסולה שלכם

### שלב 1: שאילתה בסיסית — LINQ על DbSet

```csharp
List<Student> adults = context.Students.Where(s => s.Age >= 18).OrderBy(s => s.Name).ToList();
```

נראה **בדיוק** כמו LINQ על `List<T>` שכבר הכרתם מיחידת ה-LINQ — `Where`/`OrderBy`/`ToList` עם אותו תחביר בדיוק. ההבדל קורה מאחורי הקלעים: EF Core לא מריץ לולאה על רשימה בזיכרון — הוא **מתרגם** את השרשרת הזו לשאילתת SQL אמיתית, ושולח אותה ל-SQL Server.

### שלב 2: רואים את ה-SQL שנשלח בעיניים שלכם

הריצו `dotnet run` והסתכלו בטרמינל. אין צורך להוסיף שום לוגינג — ASP.NET Core מדפיס אוטומטית, בסביבת פיתוח, כל שאילתת SQL שנשלחת. אחרי הקריאה משלב 1 תראו משהו בסגנון:

```
Executed DbCommand (3ms) [Parameters=[@__p_0='18']]
SELECT [s].[Id], [s].[Name], [s].[Age]
FROM [Students] AS [s]
WHERE [s].[Age] >= @__p_0
ORDER BY [s].[Name]
```

זו **ההוכחה** ש-`Deferred Execution` (ראו מילות מפתח) אמיתי: השורה בקוד לא "רצה" כשנכתבה — היא רצה כש-`.ToList()` הופעל, ורק אז מופיעה ההדפסה הזו. תשמרו את הקונסולה הזו פתוחה לאורך כל השיעור — היא תהיה הכלי העיקרי להוכיח את שאר השלבים.

### שלב 3: הבעיה — ניגשים ל-Navigation Property בלי Include

```csharp
Student student = context.Students.FirstOrDefault(s => s.Id == 1);
Console.WriteLine(student.Grades.Count);
```

מריצים — ובקונסולה רואים **רק שאילתה אחת**, על `Students` בלבד (בלי `Grades` בכלל). למה זה עובד בלי להתרסק? כי `Grades` הוגדר עם ברירת מחדל ריקה (`= new();` בשיעור על ה-DbContext) — אז `student.Grades.Count` מחזיר `0`, גם אם לתלמיד יש בפועל ציונים ב-DB. זו לא שגיאה גלויה — זו תוצאה **שקטה ושגויה**, בדיוק הסוג המסוכן ביותר של באג.

### שלב 4: הפתרון — Include

```csharp
Student studentWithGrades = context.Students
    .Include(s => s.Grades)
    .FirstOrDefault(s => s.Id == 1);

Console.WriteLine(studentWithGrades.Grades.Count);   // now shows the real count
```

`.Include(s => s.Grades)` אומר ל-EF Core במפורש: "כשאתה שולף את התלמיד, תטען גם את ה-Navigation Property הזו, באותה נסיעה ל-DB". עכשיו בקונסולה תראו שאילתה אחת, אבל עם `LEFT JOIN` לטבלת `Grades` בתוכה — עדיין שאילתה **אחת**, לא שתיים.

### שלב 5: בעיית ה-N+1 — רואים אותה קורית בפועל

עכשיו החלק המרכזי של השיעור — לולאה שנראית תמימה:

```csharp
List<Student> students = context.Students.ToList();   // query #1
foreach (var student in students)
{
    Console.WriteLine($"{student.Name}: {student.Grades.Count} grades");   // a SEPARATE query for every student!
}
```

הריצו את זה עם כמה תלמידים ב-DB, ותסתכלו בקונסולה: תראו שאילתת `SELECT` אחת על `Students`, ואז — שאילתת `SELECT` **נוספת ונפרדת** על `Grades`, אחת לכל תלמיד בלולאה. עם 5 תלמידים זה 6 שאילתות; עם 1,000 תלמידים (בדיוק המצב בפרודקשן) זה 1,001 שאילתות, כש-JOIN יחיד היה מספיק. זו בעיית ה-**N+1**: שאילתה אחת לרשימה (ה-"1"), ועוד שאילתה נפרדת לכל איבר בה (ה-"N").

הסיבה הטכנית: `student.Grades` בתוך הלולאה הוא גישה ל-Navigation Property שעדיין **לא נטען** (בדיוק כמו בשלב 3) — EF Core, במקום להחזיר ריק, הולך ושולף אותו **באותו רגע**, בשאילתה חדשה, בשביל כל תלמיד בנפרד.

### שלב 6: התיקון — Include לפני הלולאה, לא בתוכה

```csharp
List<Student> studentsWithGrades = context.Students.Include(s => s.Grades).ToList();   // just 1 query, with JOIN

foreach (var student in studentsWithGrades)
{
    Console.WriteLine($"{student.Name}: {student.Grades.Count} grades");   // no extra query — already loaded
}
```

אותה לולאה **בדיוק**, רק ש-`Include` נוסף לפני ה-`ToList()` הראשון. עכשיו בקונסולה: שאילתה **אחת בלבד**, עם JOIN — לא משנה אם יש 5 תלמידים או 1,000. זו הדרך לפתור N+1: `Include` לפני שהנתונים נטענים, לא תיקון בתוך הלולאה.

### שלב 7: endpoint אמיתי ובדיקה סופית

```csharp
[HttpGet("{id}/with-grades")]
public IActionResult GetWithGrades(int id)
{
    var student = _context.Students.Include(s => s.Grades).FirstOrDefault(s => s.Id == id);
    if (student == null) return NotFound();
    return Ok(student);
}
```

```bash
curl http://localhost:5123/api/students/1/with-grades
```

התשובה אמורה לכלול את `Grades` מלא, לא מערך ריק. ובקונסולה (שלב 2) — שאילתה אחת עם JOIN, בדיוק כמו בשלב 4. זו ההוכחה המלאה: מה-SQL הגולמי, דרך ה-LINQ, ועד endpoint אמיתי שעובד נכון.

## הסבר עיקרי

למה EF Core לא טוען קשרים אוטומטית — טעינה אוטומטית של **כל** Navigation Property בכל שאילתה הייתה מבזבזת ביצועים עצומים כשלא צריך את הקשר בכלל (למשל, מסך שמציג רק שמות תלמידים — לא צריך את הציונים). לכן EF Core דורש `Include` מפורש — אתם מחליטים בדיוק מה נטען, לא ה-Framework "מנחש".

בעיית ה-N+1 היא בדיוק תוצאה של גישה עצלה ל-Navigation Property בתוך לולאה — כל `student.Grades` בתוך ה-`foreach` (בלי `Include` מראש) מפעיל שאילתת SQL **נפרדת** לאותו תלמיד ספציפי. עם 100 תלמידים, זה 100 שאילתות נוספות במקום JOIN אחד — פער ביצועים עצום שקל לפספס בפיתוח מקומי (מעט נתונים, הפרש הזמנים לא מורגש) אך הרסני בפרודקשן (הרבה נתונים) — ולכן ההרגל לבדוק את הקונסולה (שלב 2) הוא כלי אבחון שישרת אתכם מעבר לשיעור הזה בלבד.

Deferred Execution — קריאה כמו `context.Students.Where(...)` **לא** שולחת שאילתה ל-DB מיד — היא רק בונה "תיאור" של השאילתה. השאילתה בפועל נשלחת רק כשקוראים `.ToList()`, `.First()`, או מנסים לעבור על התוצאה עם `foreach`. זה מאפשר לשרשר עוד `Where`/`Include` לפני שהשאילתה בפועל "יוצאת לדרך" — אך גם אומר שצריך להיזהר לא לבצע `foreach` מוקדם מדי בטעות.

## יתרונות

LINQ על `DbSet` מאפשר לכתוב שאילתות ב-C# מוכר, בלי SQL גולמי לרוב המקרים; `Include` שולט במדויק אילו קשרים נטענים, מונע בזבוז מיותר; Deferred Execution מאפשר בניית שאילתות מורכבות בהדרגה לפני שהן נשלחות ל-DB; לוגינג ה-SQL האוטומטי בפיתוח הופך בעיות ביצועים לגלויות-לעין, לא רק לתיאוריה.

## חסרונות

`Include` מיותר (טוען קשרים שלא בשימוש) מבזבז זיכרון ורוחב-פס בדיוק כמו N+1 בכיוון ההפוך; שאילתות LINQ מורכבות מאוד לפעמים מתורגמות ל-SQL לא-יעיל, ודורשות בדיקה עם Profiler.

## נקודות חשובות

• `Include` טוען Navigation Property במפורש בתוך שאילתה אחת (JOIN), במקום שאילתות נפרדות

• N+1 = שאילתה אחת + שאילתה נוספת לכל איבר בלולאה — תוצאה של גישה ל-Navigation Property בלי Include

• Deferred Execution = שאילתת LINQ על DbSet לא רצה עד `.ToList()`/`foreach`

• פתרון N+1 תמיד: `Include` לפני הלולאה, לא בדיקה/גישה בתוכה — ואפשר לוודא עם הקונסולה (שלב 2)

## טעויות נפוצות

• לגשת ל-Navigation Property בתוך `foreach` בלי `Include` מראש — הגורם הישיר ל-N+1 (ראו שלב 5)

• לחשוב שכל שאילתת LINQ על `DbSet` רצה מיד עם הכתיבה שלה — היא רצה רק ב-`.ToList()`/`foreach`

• להשתמש ב-`Include` על כל Navigation Property "ליתר ביטחון", גם כשלא בשימוש בפועל

• להתעלם מהלוגים של EF Core בקונסולה — הם הדרך הכי מהירה לגלות N+1 לפני שהוא מגיע לפרודקשן

## סיכום

LINQ על `DbSet` מתנהג כמו LINQ רגיל בתחביר, אך מתורגם ל-SQL אמיתי מאחורי הקלעים — ורץ רק ב-Deferred Execution (`.ToList()`/`foreach`). ראינו את זה בעיניים: EF Core מדפיס כל שאילתה לקונסולה בפיתוח, וזו הדרך שבה הוכחנו גם את הבעיה (שאילתה נפרדת לכל תלמיד בלולאה) וגם את הפתרון (שאילתה אחת עם JOIN). `Include` טוען Navigation Property במפורש בתוך שאילתה אחת, במקום להשאיר אותה ריקה. בעיית ה-N+1 — שאילתה נפרדת לכל איבר בלולאה במקום JOIN יחיד — היא תוצאה ישירה של גישה ל-Navigation Property בתוך `foreach` בלי `Include` קודם, ואחת ממלכודות הביצועים הנפוצות והמסוכנות ביותר בעבודה עם ORM.

## דוקומנטציה רשמית

[Microsoft Learn — Loading related data (Include)](https://learn.microsoft.com/ef/core/querying/related-data/eager)

[Microsoft Learn — Logging in EF Core](https://learn.microsoft.com/ef/core/logging-events-diagnostics/simple-logging)

---

## תרגילים

### תרגיל 1 — Include בסיסי

**המשימה:** שלפו תלמיד בודד **עם** כל ציוניו בשאילתה אחת, באמצעות `Include` (כמו בשלב 4). בלי `Include`, בדקו והדגימו ש-`Grades` ריק.

**בדיקה:** עם `Include`, `student.Grades.Count` מציג את המספר הנכון; בלעדיו, הוא מחזיר `0` גם כשיש ציונים ב-DB בפועל.

### תרגיל 2 — הדגמת N+1 בקונסולה

**המשימה:** כתבו קוד שמדגים N+1 בכוונה (כמו בשלב 5), והריצו אותו תוך מעקב אחרי הקונסולה — ספרו כמה שאילתות `SELECT` נשלחות. אחר כך תקנו עם `Include` לפני הלולאה (כמו בשלב 6), והריצו שוב.

**בדיקה:** בגרסה הראשונה, מספר השאילתות בקונסולה = מספר התלמידים + 1; בגרסה המתוקנת — שאילתה אחת בלבד, לא תלויה בכמות התלמידים.

---

## פרויקט מסכם

**המשימה:** בנו endpoint `GET api/students/with-grades` שמחזיר את כל התלמידים עם ציוניהם, יעיל מבחינת ביצועים.

**דרישות:**
1. שימוש ב-`Include` לטעינת `Grades` בשאילתה אחת, לא בלולאה
2. סינון עם `Where` — רק תלמידים עם לפחות ציון אחד (`s.Grades.Any()`)
3. מיון עם `OrderByDescending` לפי ממוצע הציונים של כל תלמיד

**בדיקה:** ה-endpoint מחזיר רק תלמידים עם ציונים, ממוינים נכון; בקונסולה רואים שאילתה אחת בלבד, לא תלויה במספר התלמידים (לא N+1).

## מה בפרק הבא

בפרק הבא — **Window Functions**: כלי SQL מתקדמים (`ROW_NUMBER`, `RANK`, `LAG`, `LEAD`) לשאילתות אנליטיות שקשה או בלתי-אפשרי לבטא עם LINQ רגיל.
