EF Core 查詢效能優化:從 SQL 轉譯、投影到 Keyset 分頁的檢查清單
EF Core 查詢效能問題常不是資料庫「突然變慢」,而是查詢在不知不覺間改到記憶體執行、載入了不需要的欄位與關聯,或先取完整集合再做分頁與統計。有效的優化順序應先確認產生的 SQL 與資料量,再處理追蹤、投影、分頁、索引和應用層演算法。
這篇文章提供可重複使用的診斷框架,不引用特定專案的筆數、耗時或成果。範例著重查詢形狀與決策原則;實際上線前,仍須使用相同資料庫引擎、接近正式環境的資料分布與參數進行驗證。
為什麼 EF Core 查詢會在記憶體執行?
當查詢過早離開 IQueryable 管線,後續篩選便可能改由應用程式逐筆處理,導致資料庫先回傳遠超過畫面所需的資料。
常見轉折點包括 AsEnumerable()、ToList() 與自訂方法。它們本身不是錯誤,但位置決定後續運算在哪裡發生。若先把集合具體化,再執行 Where、排序或分頁,資料庫就無法把這些條件轉成 SQL。
診斷時先問三個問題:
Where、Select、OrderBy與分頁發生在具體化之前還是之後?- 條件中的方法是否能被目前的 EF Core 提供者轉譯?
- 實際產生的 SQL 是否包含預期的篩選、排序與筆數限制?
動態欄位需求不應直接用反射逐筆讀取。若欄位屬於模型的一部分,應採可轉譯的運算式或 EF.Property 類型方式組成查詢,並以真實資料庫驗證。因為編譯成功只代表程式語法正確,不代表資料庫提供者一定能完成轉譯。
建議把「檢查 SQL」納入程式碼審查。審查者不只看 LINQ 是否簡潔,也要確認 SQL 的欄位、條件、排序、連接與限制符合意圖。若查詢會由高頻端點呼叫,還應保留代表性參數的執行計畫,避免只用小型開發資料誤判。
唯讀查詢為什麼要使用 AsNoTracking?
唯讀情境不需要變更追蹤;明確使用 AsNoTracking 可減少實體快照與追蹤管理工作,讓查詢目的更清楚,也降低不必要的記憶體占用。
追蹤適合「查出實體、修改屬性、再由同一個內容物件儲存」的工作流程。列表、報表、匯出、選單與大多數查詢介面通常只讀取資料,不需要讓每個實體進入追蹤器。
| 查詢用途 | 建議模式 | 原因 |
|---|---|---|
| 讀取後立即修改並儲存 | 保留追蹤 | 需要辨識變更並產生更新語句 |
| 列表或詳細資料唯讀顯示 | AsNoTracking | 不建立無用的追蹤狀態 |
| 聚合統計 | 投影純量結果 | 不必建立完整實體 |
| 重複引用同一實體的唯讀圖形 | 依需求評估身分解析 | 在去重與額外成本間取捨 |
不要把 AsNoTracking 當作掩蓋過度載入的萬靈丹。如果查詢仍讀取大量欄位、完整關聯與不必要的資料列,移除追蹤只能改善其中一部分。正確順序是先縮小資料集合與投影,再決定追蹤模式。
團隊可在資料存取層明確區分查詢與命令,讓唯讀路徑預設不追蹤,需更新時再刻意選擇追蹤。這比依賴開發者每次記得加上設定更一致,但仍要避免全域預設造成某些更新流程失去預期行為。
如何用投影避免過度載入資料?
投影應只選取畫面或計算真正需要的欄位,讓資料庫完成篩選與聚合,再回傳精簡結果,而不是載入完整實體後才丟棄大部分內容。
如果介面只要名稱、狀態與日期,就不應為了方便直接載入所有欄位與多層關聯。使用 Select 投影到專用資料模型,可同時降低傳輸量、具體化成本與序列化負擔,也讓查詢目的更容易審查。
「取得列表後只為計算數量」是典型反模式。若需求只是依條件計數,應把 Count、Any、Sum 或分組聚合下推至資料庫。若一個畫面需要多個彼此獨立的統計,可先確認資料庫連線與內容物件的使用方式,再評估安全並行;不能在同一個非執行緒安全的內容物件上任意同時執行多個查詢。
投影設計可依下列層次檢查:
- 資料列:篩選條件能否更早套用?
- 欄位:是否只選取輸出真正需要的內容?
- 關聯:能否用單一投影完成,而非載入完整物件圖?
- 聚合:能否由資料庫先計算,再回傳小型結果?
- 擴充資料:是否能先分頁,只處理當頁項目?
投影也要留意多集合關聯造成的列數膨脹。當單一查詢連接多個一對多集合,結果列可能重複組合。此時應比較拆分查詢、分階段讀取或重新設計輸出模型,並用實際資料分布量測,而不是固定認為單次查詢一定較快。
Keyset 分頁與 Offset 分頁怎麼選?
Offset 分頁適合需要跳到任意頁的介面;Keyset 分頁適合連續往後瀏覽的大型或持續變動資料集,效能與結果穩定性通常更可控。
Offset 分頁常以跳過前面若干筆再取固定筆數實作。頁數越後面,資料庫可能需要掃描或排序更多資料,而且前序資料新增或刪除時,使用者可能看到重複或遺漏。它的優點是概念直觀,也能直接顯示頁碼。
Keyset 分頁則以最後看見的排序鍵作為下一頁條件,例如先按建立時間與識別欄位排序,再取「小於上一頁最後鍵值」的資料。它不必反覆跳過大量前序資料,特別適合動態消息、事件紀錄與無限捲動。
設計 Keyset 分頁時必須做到:
- 排序穩定且唯一:單用可能重複的日期不夠,加入唯一欄位作為第二排序鍵。
- 條件方向一致:降冪排序的下一頁比較方向,必須與排序規則相符。
- 索引配合排序:複合索引欄位順序要支援篩選與排序形狀。
- 游標取自原始順序:若頁面取得後又在記憶體重排,不可直接拿顯示上的最後一筆當下一頁依據。
- 先分頁再補資料:只對當頁項目取得名稱、統計或其他擴充資訊。
| 需求 | Offset | Keyset |
|---|---|---|
| 任意跳頁 | 適合 | 不直接支援 |
| 連續下一頁 | 可用 | 適合 |
| 大型資料後段 | 成本可能上升 | 通常較穩定 |
| 資料持續新增 | 可能重複或遺漏 | 搭配穩定鍵較可靠 |
| 實作複雜度 | 較低 | 需嚴謹設計排序鍵 |
有「分頁參數」不代表真的在資料庫分頁。務必確認 SQL 中存在對應的排序、條件與筆數限制;若程式先取得完整資料再切片,介面雖然只顯示一頁,後端成本仍是全量讀取。
索引失效時應該怎麼診斷?
索引診斷要從實際查詢條件與執行計畫出發,特別檢查欄位上的函數、跨欄位 OR、型別轉換,以及複合索引順序是否破壞可搜尋性。
當條件先對資料欄位做字串替換、日期轉換或計算,資料庫可能無法直接沿著原始索引定位。比起在每次查詢即時計算,可評估將標準化結果存成可索引欄位,或在資料寫入時完成正規化。實作方式依資料庫能力而異,變更前要驗證讀寫成本與一致性。
跨多欄位的 OR 也可能使最佳化器難以選擇有效路徑。可比較以下方案:
- 各條件分別走適合的索引,再合併識別欄位。
- 重新設計可搜尋的標準化欄位。
- 將用途不同的搜尋拆成明確模式,避免一個端點包辦所有條件。
- 使用資料庫支援的全文或專用搜尋能力,但先確認一致性需求。
複合索引不是把常用欄位全部放進去。欄位順序需根據等值條件、範圍條件與排序方式安排;同一組欄位換順序,可能支援完全不同的查詢。索引也會增加寫入與儲存成本,因此每個新增索引都應對應到具體查詢與執行計畫。
不要用開發環境的小資料表判斷索引價值。資料量、值的分布、熱門參數與快取狀態都會改變計畫。驗證應包含代表性的高選擇性與低選擇性參數,並確認統計資料更新後,計畫仍符合預期。
DbContext 生命週期如何影響效能與穩定性?
DbContext 應維持清楚且短暫的工作單元生命週期,由依賴注入管理建立與釋放;它不適合被多執行緒共享,也不應成為程序級常駐物件。
每次查詢都自行建立內容物件,看似能避免狀態污染,卻容易讓設定、交易與測試難以一致。相反地,長時間持有同一個內容物件會累積追蹤狀態,並增加並行誤用風險。一般網頁請求可把生命週期對齊單一工作單元,背景處理則為每個工作單元建立獨立範圍。
使用內容物件池時,重用的是可安全重設的執行個體,不代表業務狀態可以跨請求保留。任何依租戶、使用者或請求而變的狀態,都要有明確設定與清理方式。資料庫連線設定也應由正式設定來源與部署程序管理,不要用無同步保護的靜態快取自行保存。
生命週期檢查清單:
- 每個工作單元有明確的建立與釋放邊界。
- 不在多個並行工作間共用同一個 DbContext。
- 查詢取消能向下傳遞,請求中止後不繼續占用資料庫。
- 交易範圍只包住必要操作,不在其中執行外部呼叫。
- 連線池、內容物件池與資料庫上限一起做容量規劃。
- 變更設定後的部署與重啟行為已有驗證程序。
如何建立可重複的效能優化流程?
可重複的優化流程應先建立可重現案例,依序檢查 SQL、資料量、執行計畫與應用成本,每次只改一個主要因素並保留前後證據。
建議依以下步驟執行:
- 固定案例:記錄查詢入口、代表性參數、資料分布與逾時條件。
- 取得 SQL:確認篩選、排序、聚合與分頁確實下推。
- 檢查資料量:比較回傳列數、欄位數與畫面實際需求。
- 查看計畫:辨識掃描、排序、連接與索引使用情形。
- 縮小查詢:先處理過早具體化、投影、關聯與分頁。
- 調整索引:讓索引服務已確認的查詢形狀,而非猜測欄位。
- 檢查應用層:找出熱迴圈中的線性搜尋、重複轉換與序列化成本。
- 回歸驗證:確認結果正確、排序穩定、記憶體可控且無新增查詢爆量。
量測時至少分開資料庫執行、資料傳輸、物件具體化與回應序列化。只看端點總時間,無法判斷改善來自哪一層,也可能把快取暖機誤認為程式優化。若要比較前後版本,應使用相同資料、相同參數與相同測試程序。
應用層資料結構也會放大成本。若熱迴圈反覆用 List 做成員查找,資料增加後可能形成平方級工作量;改用 HashSet 或預先建立索引字典,通常能把意圖表達得更清楚。不過這類改善應排在「不要取回無用資料」之後,避免只加速一段原本不該存在的全量處理。
EF Core 查詢效能上線前檢查清單
上線前應證明查詢在資料庫端完成必要工作、只回傳所需資料、具備穩定排序與合適索引,並在代表性資料分布下通過回歸測試。
SQL 與資料量
- 所有主要篩選都位於具體化之前。
- 自訂運算式已確認能由目前資料庫提供者轉譯。
- SQL 只選必要欄位,沒有無目的的完整物件圖。
- 統計使用資料庫端聚合,不為了計數載入列表。
- 已檢查迴圈內是否產生額外查詢。
分頁與索引
- 排序鍵穩定,重複值有唯一欄位輔助排序。
- Keyset 游標方向與排序一致,且取自原始順序。
- 只對當頁資料執行擴充處理。
- 執行計畫使用預期索引,代表性參數均已驗證。
- 新增索引的寫入與儲存成本已納入評估。
生命週期與回歸
- 唯讀路徑採合適的不追蹤模式。
- DbContext 不跨並行工作共享。
- 取消、逾時與交易邊界行為明確。
- 測試涵蓋空資料、重複排序鍵、大結果集與資料持續新增。
- 優化前後證據可重現,沒有以單次快取結果下結論。
結論:先修查詢形狀,再談微幅調校
EF Core 查詢效能的優先順序是讓運算留在正確的位置:資料庫負責篩選、排序與聚合,應用程式只接收必要結果並處理業務呈現。
先確認 IQueryable 管線沒有過早中斷,再以投影縮小欄位與關聯,接著設計穩定分頁並用執行計畫驗證索引。最後才處理追蹤細節、內容物件池與應用層資料結構。這個順序能避免在錯誤的查詢形狀上進行微幅調校,也讓每項改善都能以相同案例驗證。
內部連結建議:資料規模與分布已需要水平擴充決策時,可延伸閱讀「MongoDB 分片決策指南」;若優化涉及發布與回復程序,接續「建立可稽核的 CI/CD 發布流程」建立變更品質閘門。