EFCORE - 關聯載入的海流
這是一份「資料載入的潮汐圖」。我們將從 Lazy 與 Eager 的對比開始,一路探索 N+1 問題的成因、查詢快取的脈動、再航向非對應類型與原生 SQL 的深處…
Lazy Loading(延遲載入)是一種「等你真的用到資料時再去撈」的策略
你查出所有英國的客戶(Customers),但還沒看他們的訂單(Orders)。Lazy Loading 的邏輯是:「先不用撈訂單,等你真的要看時再查就好
1 | var customers = context.Customers.Where(c => c.Country == "UK").ToList(); |
但實際上會發生兩個 SQL 查詢
1 | --先查所有客戶 |

若你要顯示所有客戶的訂單數量,就會發出 N+1 次查詢(一筆主查詢 + N 次子查詢)。這種行為在資料量稍大的情況下會造成效能雪崩。
N+1 問題其實就是「每次載入子集合都觸發一次 SQL」
1 | var customers = context.Customers.ToList(); |

Eager Loading(積極載入)則是在查詢時就「一次把所有關聯資料撈回來」
1 | var customers = context.Customers |
⚠️ 缺點
- 回傳資料量龐大(尤其多層 Include 時)
- SQL 複雜、可能有重複資料
- 若只是少數欄位會用到,等於浪費載入時間與記憶體

只取你真正需要的資料,而非整個實體
1 | var customers = context.Customers |
不會載入整個 Orders 集合,只抓數量(由 SQL 層完成)

當多層 Include() 導致查詢過於複雜時,可以分成兩段查
1 | var customers = context.Customers |
EF Core 的追蹤機制(Change Tracker)會自動把兩者關聯起來(根據主鍵/外鍵匹配)。這樣可以避免龐大的 JOIN SQL,同時保持關聯一致

你有一組 ID 想查出對應的資料
1 | int[] ids = new int[10000]; // 一大堆要查的 Id |
這樣的寫法非常常見,但在 Entity Framework 的查詢快取機制裡,這種查詢無法被快取。
原因在於,ids 是一個 記憶體中的集合(in-memory collection),而 EF 會認為這些值是「揮發性(volatile)」的,也就是說,每次查詢都有可能不同
EF 沒辦法確定「這次查詢的集合」跟「上次」是不是完全一樣。因此它不會把這個查詢結果存進查詢計劃快取中。
每次執行這樣的查詢,EF 都會重新走一次「查詢編譯流程」,重新產生 SQL。查詢計劃快取(Query Plan Cache)無法重用,每次查詢都要經過編譯 → 效能下降

| 狀況 | 建議做法 |
|---|---|
| ✅ 小型集合(例如 < 100) | 直接用 .Contains() 就可以,影響不大 |
| ⚠️ 中型集合(100~1000) | 可改成一次查詢分批(例如每 200 筆) |
| 🚫 大型集合(>1000) | 建議用暫時表(Temporary Table)或批次上傳資料庫處理 |
| ✅ 頻繁查詢同樣集合 | 可先在資料庫建立固定表或快取結果 |
| ⚙️ 若查詢內容動態 | 不可避免重新編譯,但可利用 EF6 優化版本減少損耗 |

- 「對應(Mapped)」:代表這個型別有在 Entity Framework 的模型中出現。例如 MyEntity 是資料庫中的一張表。
- 「非對應(Non-Mapped)」:代表這個類別只是你程式裡的一個普通類別,它跟資料庫沒有任何 Mapping 關係。
1 | using (var context = new MyContext()) |
現在,EF 可以辨認出 myValue 是一個固定值(或可參數化的變數);查詢結構是穩定的;它就能生成一個「可重用的查詢計畫」,並放進查詢計劃快取中

| 類別 | 查詢方式 | 特性 |
|---|---|---|
| 🔹 LINQ 層級查詢(語法友善) | LINQ to Entities、NoTracking LINQ、CompiledQuery | 使用 C# 語法撰寫,編譯時期安全 |
| 🔹 Entity SQL 層級查詢(抽象 SQL) | ObjectQuery ESQL、EntityCommand ESQL | 類似 SQL,但基於「概念模型」 |
| 🔹 原生 SQL 層級查詢(效能優先) | ExecuteStoreQuery、SqlQuery | 直接對資料庫執行 SQL,最高效能但最少保護 |

🔹 LINQ to Entities 查詢
1 | var q = context.Products |
- 最直覺、最容易維護
- 編譯時期會做型別檢查
- 支援 CUD(Create / Update / Delete)操作
- 效能良好(會使用查詢快取與最佳化器)
- 某些 LINQ 語法(如 DefaultIfEmpty)產生的 OUTER JOIN 比 SQL 複雜
- 不支援原生的 LIKE 模式比對(要用 SqlFunctions.PatIndex 或 EF.Functions.Like)
- 適合一般應用程式的查詢與操作

🔹 無追蹤(No Tracking)LINQ 查詢
1 | var q = context.Products |
- 效能比一般 LINQ 快,因為不需要追蹤變更
- 同樣能完整具體化物件
- 適合唯讀場景(例如報表、查詢頁)
- 不能做 CUD 操作(因為物件不被追蹤)
- 某些查詢模式仍有 LINQ 的技術限制
💡 小技巧
即使沒指定 AsNoTracking(),若查詢只投影純量(如匿名型別),EF 也不會追蹤
1 | var q = context.Products |
![]()
🔹 使用 EntityCommand 執行 Entity SQL
1 | EntityCommand cmd = eConn.CreateCommand(); |
- 在 .NET 4.0 時期可使用查詢計劃快取(早期優化技巧)
- 可手動控制讀取(DataReader 型態)
- 不支援 CUD
- 結果不會自動具體化(要自己轉換)。
- 字串查詢易錯、難維護。
- 適合需要低階控制查詢結果(例如串接原始管道)。

🔹 SqlQuery 與 ExecuteStoreQuery
這是直接執行「原生 SQL 查詢」的方式
1 | 📘 SqlQuery(DbContext) |
- 最快效能,因為完全跳過 EF 查詢翻譯器(Query Compiler)。
- 可以寫出最精確、最優化的 SQL。
- DbSet.SqlQuery 可自動追蹤,支援 CUD。
- 文字查詢容易出錯。
- 不具跨資料庫移植性。
- 若有繼承結構,需自己處理對應條件。
- 適合效能導向場景(大量報表、批次查詢)
- 需要手動優化 SQL 的應用。

| 想要… | 請用… |
|---|---|
| 最高效能 | ExecuteStoreQuery / SqlQuery |
| 最乾淨語法 | LINQ to Entities |
| 最快唯讀查詢 | AsNoTracking() |
| 高頻重複查詢 | CompiledQuery |
| 動態查詢組合 | Entity SQL |










