🔹 EF Core 為什麼需要 Tracking?

在預設情況下,EF Core 查詢出來的物件會被放進 Change Tracker,它會記錄

  • 哪些屬性被讀進來
  • 哪些屬性值被修改
  • 哪些 Entity 跟其他 Entity 有關聯

這樣 EF Core 才能在 SaveChanges() 的時候,自動知道要 Update/Insert/Delete 哪些資料。

🔹 Tracking 帶來的效能負擔

Tracking 雖方便,但會多做很多事情

  • 建立快照 (Snapshot):每個 Entity 要存一份原始值,方便比對
  • 變更偵測 (Change Detection):每次存取或更新,EF 都要比對目前值與快照值
  • 關聯維護 (Relationship Fixup):要維護 Navigation Properties (例如 Order → Customer)

如果只是單純查詢 (Read-Only),這些動作完全是 多餘的成本。尤其當查詢回來的資料很多(成千上萬筆),這些「追蹤資料」會嚴重拖慢效能,佔用 CPU + 記憶體

F

🔹 AsNoTracking() 怎麼提高效能?

當你呼叫 AsNoTracking(),EF 就會 跳過 Change Tracker,效果有幾點:

  • 不建立快照 → 減少記憶體用量
  • 不做變更比對 → 查詢速度變快
  • 不維護關聯 → CPU 負擔變小
  • 避免 GC 壓力 → 整體系統效能提升
1
2
3
4
5
6
7
8
9
10
// 預設有 Tracking
var customers1 = context.Customers
.Where(c => c.IsActive)
.ToList();

// 使用 AsNoTracking
var customers2 = context.Customers
.AsNoTracking()
.Where(c => c.IsActive)
.ToList();

F

🔹 一般 Update(傳統做法)是怎樣?

要更新一筆訂單,EF Core 的一般流程是這樣:

  • 先查資料(SELECT) → 把訂單資料抓回來。
  • 在記憶體改值(order.Status = Paid)。
  • 呼叫 SaveChanges() → EF Core 幫你比對「原本 vs 改後」,然後產生 UPDATE。

這個流程需要「先查後改」,有時候只為了改一個欄位卻多了一次 SELECT,效率不好。

F

🔹 ExecuteUpdateAsync 的做法

直接跳過「先查資料」這步。你告訴 EF Core 哪些資料要更新(Where 條件)、要改哪些欄位(SetProperty)EF Core 就直接丟一條 SQL UPDATE 給資料庫。

1
2
3
4
5
await db.Orders
.Where(o => o.Status == OrderStatus.Pending && o.Id == id)
.ExecuteUpdateAsync(setters => setters
.SetProperty(o => o.Status, o => OrderStatus.Paid)
.SetProperty(o => o.PaidAt, o => DateTime.UtcNow));

生成的 SQL 大概會長這樣

1
2
3
4
UPDATE Orders
SET Status = 'Paid',
PaidAt = '2025-10-01 12:00:00'
WHERE Status = 'Pending' AND Id = 123;

又或者,批次更新

1
2
3
await db.Users
.Where(u => u.LastLogin < DateTime.UtcNow.AddYears(-1))
.ExecuteUpdateAsync(s => s.SetProperty(u => u.IsActive, u => false));

👉 ExecuteUpdateAsync 就是「直接發 SQL UPDATE,不經過 EF Core 的實體/追蹤機制」。

F

EF Core 的 Include() 用單一查詢時 → 很容易遇到「一個主表資料被重複很多次」的情況,這就是笛卡兒爆炸

  • 一對多 (One-to-Many) 例如:User → Orders (一個用戶有很多訂單)。User 會被重複列出很多次。
  • 多對多 (Many-to-Many) 例如:User ↔ Roles (一個用戶有多個角色)。 User 會被重複列出更多次,尤其角色數量龐大時,效能問題更明顯。

User A:3 筆訂單(Order 1、2、3)
User B:2 筆訂單(Order 4、5)

🔹 普通 Include

1
2
3
var users = _db.Users
.Include(u => u.Orders)
.ToList();

SQL 結果(Join 展開)

UserId UserName OrderId Amount
A Alice 1 100
A Alice 2 200
A Alice 3 300
B Bob 4 50
B Bob 5 75

EF Core 物件圖(去重)

1
2
3
users.Count = 2
Alice.Orders = [1, 2, 3]
Bob.Orders = [4, 5]

✅ 好處

程式方便直接使用完整物件

❌ 壞處

SQL 行數 = N * M → 當 Order 數量很多時,資料量爆炸

F

🔹 投影成 DTO,只取必要資料

1
2
3
4
5
6
7
var users = _db.Users
.Select(u => new {
u.Id,
u.Name,
Orders = u.Orders.Select(o => new { o.Id, o.Amount }).ToList()
})
.ToList();

EF Core 會生成 子查詢 / 二段查詢,或可能生成一個 JSON 聚合(依 Provider 版本不同)

UserId UserName Orders (子查詢聚合)
A Alice [{1,100},{2,200},{3,300}]
B Bob [{4,50},{5,75}]

EF Core / DTO 物件

1
2
3
users.Count = 2
Alice.Orders = [ {Id=1, Amount=100}, {2,200}, {3,300} ]
Bob.Orders = [ {4,50}, {5,75} ]

✅ 好處

  • SQL 直接聚合關聯資料,不會 N * M 爆炸
  • 可以只取必要欄位(Id、Amount)

❌ 壞處

  • 無法直接操作實體,只能用 DTO
  • 無法追蹤 Entity(沒辦法 Update 回 DbContext)

F

🔹 分兩次查詢

Explicit Load

1
2
var users = _db.Users.ToList(); // 先撈 User
_db.Entry(users[0]).Collection(u => u.Orders).Load(); // 再載入 Order

Split Query(EF Core 5+)

1
2
3
4
var users = _db.Users
.Include(u => u.Orders)
.AsSplitQuery()
.ToList();
UserId UserName
A Alice
B Bob
OrderId UserId Amount
1 A 100
2 A 200
3 A 300
4 B 50
5 B 75

EF Core 物件圖

1
2
3
users.Count = 2
Alice.Orders = [1,2,3]
Bob.Orders = [4,5]

✅ 好處

  • SQL 行數 = N + M → 避免 N * M 爆炸
  • 可以用實體(可 Update 回 DbContext)
  • 支援 Include 多層關聯

❌ 壞處

會發送多個 SQL 查詢(Trade-off:網路往返多一次,但每筆行數少)

F

操作資料庫屬於 I/O 需求,同步 API 在資料庫執行期間會封鎖執行緒,增加執行緒需求,同時無法避免執行緒內容切換


🔹 I/O = Input/Output

在程式世界裡通常指 跟外部資源溝通,而不是 CPU 算數學,因為 EF Core 在你呼叫 .ToListAsync() 或 .SaveChangesAsync() 時,背後是透過 ADO.NET → TCP/IP → SQL Server 去跟資料庫伺服器溝通。
操作是依照 Entity Framework (EF Core) → 高階 ORM (Object Relational Mapper),讓你用 C# 的物件去操作資料,不用自己寫 SQL。


🔹 ADO.NET

.NET 內建的底層資料存取 API,負責真正跟資料庫「溝通」。EF Core 只是包裝 (Wrapper),實際執行 SQL 查詢的還是 ADO.NET,ADO.NET 提供一組 API,讓程式能透過 資料庫驅動程式 (Data Provider) 去連接資料庫
主要元件:

  • Connection → 跟資料庫建立連線 (例如 SqlConnection)
  • Command → 執行 SQL 指令 (例如 SqlCommand)
  • DataReader → 一筆一筆讀資料
  • DataAdapter / DataSet → (傳統的方式) 批次存取資料

ADO.NET = 鐵路系統(火車頭、鐵軌) → 直接跟資料庫搬運資料
EF Core = 高鐵 APP → 你只要輸入起點終點,它幫你產生車票、安排火車,實際的運輸還是靠鐵路

gf

🔹 同步 API

呼叫後,執行緒會「卡住」直到 DB 把結果回傳


🔹 非同步 API (Async)

呼叫後,執行緒會釋放回 ThreadPool,等 DB 回應時再喚醒繼續,在高併發(例如 Web API 同時上百請求)時,非同步能避免 ThreadPool 被塞爆。因為少了不必要的閒置執行緒,確實會降低 OS 的排程壓力
但要注意非同步本身有開銷,Async/Await 會產生狀態機 (state machine),如果只是單一 Console App 查一次 DB → await 可能反而更慢

gf

🔹 IQueryable

代表「一個還沒真正執行的查詢 (Query Expression)」,延遲執行 (Deferred Execution) → 直到你 .ToList() 或 foreach 才真的送 SQL 到資料庫,可以在送出前繼續調整查詢,最後只打一個 SQL

1
2
3
4
5
6
7
IQueryable<User> query = context.Users;

// 動態加條件
if (isActiveOnly)
query = query.Where(u => u.IsActive);

var result = query.ToList(); // 這裡才真的送 SQL

過濾條件在 DB 端執行,減少網路傳輸量 & 記憶體負擔

gf

🔹 IEnumerable

IEnumerable 代表「已經在記憶體中可以逐筆讀取的集合」,查詢可能已經執行,資料已經被載入到記憶體,後續的 .Where() 會在記憶體裡過濾,而不是轉成 SQL,如果資料很多,你先 .ToList() 拉回來,再過濾 → 浪費網路頻寬 & 記憶體。適合小資料量,或你已經確定一定要把所有資料都載進來

🔹 IList

IList 是「在記憶體裡可操作的清單」,支援 增刪改查,不涉及 SQL,它就是 C# 的一個集合 (通常是 List),沒有查詢優化的空間,因為資料已經全部在記憶體裡

gf