這是一份「資料載入的潮汐圖」。我們將從 Lazy 與 Eager 的對比開始,一路探索 N+1 問題的成因、查詢快取的脈動、再航向非對應類型與原生 SQL 的深處…

Lazy Loading(延遲載入)是一種「等你真的用到資料時再去撈」的策略

你查出所有英國的客戶(Customers),但還沒看他們的訂單(Orders)。Lazy Loading 的邏輯是:「先不用撈訂單,等你真的要看時再查就好

1
2
var customers = context.Customers.Where(c => c.Country == "UK").ToList();
Console.WriteLine(customers.First().Orders.Count);

但實際上會發生兩個 SQL 查詢

1
2
3
4
--先查所有客戶
SELECT * FROM Customers WHERE Country = 'UK'
--當你第一次存取 Orders 屬性時
SELECT * FROM Orders WHERE CustomerID = 'AROUT'

f

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

1
2
3
4
5
var customers = context.Customers.ToList();
foreach (var c in customers)
{
Console.WriteLine($"{c.Name} has {c.Orders.Count} orders");
}

f

Eager Loading(積極載入)則是在查詢時就「一次把所有關聯資料撈回來」

1
2
3
4
var customers = context.Customers
.Include(c => c.Orders)
.Where(c => c.Country == "UK")
.ToList();

⚠️ 缺點

  • 回傳資料量龐大(尤其多層 Include 時)
  • SQL 複雜、可能有重複資料
  • 若只是少數欄位會用到,等於浪費載入時間與記憶體

f

只取你真正需要的資料,而非整個實體

1
2
3
4
5
6
7
var customers = context.Customers
.Select(c => new
{
c.Name,
OrderCount = c.Orders.Count
})
.ToList();

不會載入整個 Orders 集合,只抓數量(由 SQL 層完成)

f

當多層 Include() 導致查詢過於複雜時,可以分成兩段查

1
2
3
4
5
6
7
var customers = context.Customers
.Where(c => c.Country == "UK")
.ToList();

var orders = context.Orders
.Where(o => o.Customer.Country == "UK")
.ToList();

EF Core 的追蹤機制(Change Tracker)會自動把兩者關聯起來(根據主鍵/外鍵匹配)。這樣可以避免龐大的 JOIN SQL,同時保持關聯一致

f

你有一組 ID 想查出對應的資料

1
2
3
4
5
6
7
8
int[] ids = new int[10000]; // 一大堆要查的 Id
using (var context = new MyContext())
{
var query = context.MyEntities
.Where(entity => ids.Contains(entity.Id));

var results = query.ToList();
}

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

f

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

f

  • 「對應(Mapped)」:代表這個型別有在 Entity Framework 的模型中出現。例如 MyEntity 是資料庫中的一張表。
  • 「非對應(Non-Mapped)」:代表這個類別只是你程式裡的一個普通類別,它跟資料庫沒有任何 Mapping 關係。
1
2
3
4
5
6
7
8
9
10
11
using (var context = new MyContext())
{
var myObject = new NonMappedType();
var myValue = myObject.MyProperty; // ✅ 取出屬性值,變成區域變數

var query = from entity in context.MyEntities
where entity.Name.StartsWith(myValue)
select entity;

var results = query.ToList();
}

現在,EF 可以辨認出 myValue 是一個固定值(或可參數化的變數);查詢結構是穩定的;它就能生成一個「可重用的查詢計畫」,並放進查詢計劃快取中

f

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

f

🔹 LINQ to Entities 查詢

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

f

🔹 無追蹤(No Tracking)LINQ 查詢

1
2
3
var q = context.Products
.AsNoTracking()
.Where(p => p.Category.CategoryName == "Beverages");
  • 效能比一般 LINQ 快,因為不需要追蹤變更
  • 同樣能完整具體化物件
  • 適合唯讀場景(例如報表、查詢頁)
  • 不能做 CUD 操作(因為物件不被追蹤)
  • 某些查詢模式仍有 LINQ 的技術限制

💡 小技巧

即使沒指定 AsNoTracking(),若查詢只投影純量(如匿名型別),EF 也不會追蹤

1
2
3
var q = context.Products
.Where(p => p.Category.CategoryName == "Beverages")
.Select(p => new { p.ProductName }); // 自動 NoTracking

f

🔹 使用 EntityCommand 執行 Entity SQL

1
2
3
4
5
6
7
8
9
10
EntityCommand cmd = eConn.CreateCommand();
cmd.CommandText = "SELECT p FROM NorthwindEntities.Products AS p WHERE p.Category.CategoryName = 'Beverages'";

using (EntityDataReader reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess))
{
while (reader.Read())
{
// 手動 materialize 物件
}
}
  • 在 .NET 4.0 時期可使用查詢計劃快取(早期優化技巧)
  • 可手動控制讀取(DataReader 型態)
  • 不支援 CUD
  • 結果不會自動具體化(要自己轉換)。
  • 字串查詢易錯、難維護。
  • 適合需要低階控制查詢結果(例如串接原始管道)。

f

🔹 SqlQuery 與 ExecuteStoreQuery

這是直接執行「原生 SQL 查詢」的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
📘 SqlQuery(DbContext)
// 不追蹤(Database 層級)
var q1 = context.Database.SqlQuery<Product>("SELECT * FROM Products");

// 有追蹤(DbSet 層級)
var q2 = context.Products.SqlQuery("SELECT * FROM Products");

📘 ExecuteStoreQuery(ObjectContext)
var beverages = context.ExecuteStoreQuery<Product>(
@"SELECT P.*
FROM Products AS P
JOIN Categories AS C ON P.CategoryID = C.CategoryID
WHERE C.CategoryName = 'Beverages'");
  • 最快效能,因為完全跳過 EF 查詢翻譯器(Query Compiler)。
  • 可以寫出最精確、最優化的 SQL。
  • DbSet.SqlQuery 可自動追蹤,支援 CUD。
  • 文字查詢容易出錯。
  • 不具跨資料庫移植性。
  • 若有繼承結構,需自己處理對應條件。
  • 適合效能導向場景(大量報表、批次查詢)
  • 需要手動優化 SQL 的應用。

f

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

f

f