LINQ 延遲執行與效能陷阱
什麼是延遲執行?
var query = db.Users.Where(u => u.Age > 18); // ← 這行不會查資料庫!
// query 只是一個「查詢計畫」,還沒執行
var list = query.ToList(); // ← 這行才真的送 SQL 到資料庫
延遲執行(Deferred Execution):LINQ 查詢只在你「列舉」時才執行。
IEnumerable vs IQueryable
| IEnumerable |
IQueryable |
|
|---|---|---|
| 執行位置 | 記憶體(C# 端) | 資料庫(SQL 端) |
| 篩選時機 | 先全部載入,再篩選 | 篩選條件轉成 SQL |
| 適合 | 記憶體中的集合 | EF Core 資料庫查詢 |
// ❌ 效能災難:載入全部使用者到記憶體再篩選
IEnumerable<User> users = db.Users; // 載入 100 萬筆
var adults = users.Where(u => u.Age > 18); // 在 C# 記憶體中篩選
// ✅ 正確:篩選條件在資料庫執行
IQueryable<User> users = db.Users; // 還沒查
var adults = users.Where(u => u.Age > 18).ToList(); // WHERE age > 18(SQL 端篩選)
常見陷阱
1. 多次列舉
var query = db.Users.Where(u => u.IsActive);
var count = query.Count(); // ← 查一次 DB
var list = query.ToList(); // ← 又查一次 DB!
var first = query.First(); // ← 又查一次!
// ✅ 先 ToList(),再對記憶體操作
var list = query.ToList(); // 查一次
var count = list.Count; // 記憶體操作
var first = list.First(); // 記憶體操作
2. Select N+1
// ❌ 每個 Order 都會查一次 Customer(N+1 問題)
var orders = db.Orders.ToList();
foreach (var o in orders) {
Console.WriteLine(o.Customer.Name); // 每次都查 DB!
}
// ✅ Include 一次載入
var orders = db.Orders.Include(o => o.Customer).ToList();
3. 在迴圈裡用 LINQ 查詢
// ❌ 每次迴圈都送一次 SQL
foreach (var id in userIds) {
var user = db.Users.FirstOrDefault(u => u.Id == id); // N 次查詢
}
// ✅ 一次撈完
var users = db.Users.Where(u => userIds.Contains(u.Id)).ToList(); // 1 次查詢
ToList() vs AsEnumerable() vs AsNoTracking()
.ToList() // 立即執行,載入到記憶體(List<T>)
.AsEnumerable() // 切換到 LINQ to Objects(之後的操作在記憶體)
.AsNoTracking() // 不追蹤實體變化(唯讀查詢效能提升 30-50%)
原則:只讀不改的查詢,一律加
.AsNoTracking()。