EFCORE - 効率の詩
🔹 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 + 記憶體
![]()
🔹 AsNoTracking() 怎麼提高效能?
當你呼叫 AsNoTracking(),EF 就會 跳過 Change Tracker,效果有幾點:
- 不建立快照 → 減少記憶體用量
- 不做變更比對 → 查詢速度變快
- 不維護關聯 → CPU 負擔變小
- 避免 GC 壓力 → 整體系統效能提升
1 | // 預設有 Tracking |
![]()
🔹 一般 Update(傳統做法)是怎樣?
要更新一筆訂單,EF Core 的一般流程是這樣:
- 先查資料(SELECT) → 把訂單資料抓回來。
- 在記憶體改值(order.Status = Paid)。
- 呼叫 SaveChanges() → EF Core 幫你比對「原本 vs 改後」,然後產生 UPDATE。
這個流程需要「先查後改」,有時候只為了改一個欄位卻多了一次 SELECT,效率不好。

🔹 ExecuteUpdateAsync 的做法
直接跳過「先查資料」這步。你告訴 EF Core 哪些資料要更新(Where 條件)、要改哪些欄位(SetProperty)EF Core 就直接丟一條 SQL UPDATE 給資料庫。
1 | await db.Orders |
生成的 SQL 大概會長這樣
1 | UPDATE Orders |
又或者,批次更新
1 | await db.Users |
👉 ExecuteUpdateAsync 就是「直接發 SQL UPDATE,不經過 EF Core 的實體/追蹤機制」。

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 | var users = _db.Users |
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 | users.Count = 2 |
✅ 好處
程式方便直接使用完整物件
❌ 壞處
SQL 行數 = N * M → 當 Order 數量很多時,資料量爆炸

🔹 投影成 DTO,只取必要資料
1 | var users = _db.Users |
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 | users.Count = 2 |
✅ 好處
- SQL 直接聚合關聯資料,不會 N * M 爆炸
- 可以只取必要欄位(Id、Amount)
❌ 壞處
- 無法直接操作實體,只能用 DTO
- 無法追蹤 Entity(沒辦法 Update 回 DbContext)

🔹 分兩次查詢
Explicit Load
1 | var users = _db.Users.ToList(); // 先撈 User |
Split Query(EF Core 5+)
1 | var users = _db.Users |
| 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 | users.Count = 2 |
✅ 好處
- SQL 行數 = N + M → 避免 N * M 爆炸
- 可以用實體(可 Update 回 DbContext)
- 支援 Include 多層關聯
❌ 壞處
會發送多個 SQL 查詢(Trade-off:網路往返多一次,但每筆行數少)

操作資料庫屬於 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 → 你只要輸入起點終點,它幫你產生車票、安排火車,實際的運輸還是靠鐵路

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

🔹 IQueryable
代表「一個還沒真正執行的查詢 (Query Expression)」,延遲執行 (Deferred Execution) → 直到你 .ToList() 或 foreach 才真的送 SQL 到資料庫,可以在送出前繼續調整查詢,最後只打一個 SQL
1 | IQueryable<User> query = context.Users; |
過濾條件在 DB 端執行,減少網路傳輸量 & 記憶體負擔

🔹 IEnumerable
IEnumerable
🔹 IList
IList











