最近在修改一個.NET Core的項目,其中ORM用的EF Core,在一次查詢分頁中,遇到了一個很奇怪的問題,每次查詢都很慢,明明已經按照某個編號字段Group By並且還做了分頁,為啥查詢還這么的慢呢?
首選我想當的解決方案就是為 每個條件查詢字段添加索引,但是依然無效,還是很慢;然后查看log日志,仔細核對EF生成的sql,發現了生成的sql根本就沒有Group by 以及后面的分頁操作也沒有生成,sql只是到where條件判斷之后就結束了,相當於查詢了所有結果,當然展示的數據是我們想要的結果,所以可以肯定的是Group BY 之后的操作是在內存中處理的
原始EF 查詢如下
var groupList_one = dbConContext.TMemberWelcomeLog.AsNoTracking().Where(p => p.Status == 0 && p.MerchantCode == "SH202009094127602" && p.CreateDateTime >= startTime && p.CreateDateTime <= DateTime.Now). GroupBy(p => p.MemberCode); var list_one = await groupList_one.OrderByDescending(r => r.Count()).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(); var total_one = list_one.Count();
上面的語句生成的sql如下:
SELECT [t].[Id], [t].[CreateDateTime], [t].[EnterDateTime], [t].[EnterImg], [t].[LeaveDateTime], [t].[LeaveImg], [t].[MemberCode], [t].[MerchantCode], [t].[Status], [t].[StayTime], [t].[StoreCode], [t].[UpdateDateTime] FROM[T_MemberWelcomeLog] AS[t] WHERE((([t].[Status] = 0) AND([t].[MerchantCode] = N'SH202009094127602')) AND([t].[CreateDateTime] >= @__startTime_0)) AND([t].[CreateDateTime] <= GETDATE())
從上面的語句來看,很顯然是沒有生成Group by及以后的分頁語句,為什么會是這樣呢???
注意: EF CORE 3.0及以上版本會報錯:Unable to translate the given 'GroupBy' pattern. Call 'AsEnumerable' before 'GroupBy' to evaluate it client-side
於是查詢官方文檔【客戶端與服務器評估】
大概意思是:
EF CORE會盡可能的嘗試服務器評估,生成等效的數據庫查詢SQL,但是有些方法是客戶端特有的處理方式,例如在客戶端寫了一個特殊的方法,去處理EFCore查詢中的某一個字段,這個時候服務端是無法預知結果,並轉換成對應的sql,這個時候EF CORE會報上面的那個錯
那么如何處理上面這個問題呢?官方給出了解決方案,就是需要顯示客戶端評估,官方話語是:在這種情況下,通過調用 AsEnumerable 或 ToList 等方法(若為異步,則調用 AsAsyncEnumerable 或 ToListAsync),以顯式方式選擇進行客戶端評估,這個結果就是我們上面的查詢列子相同,會把AsEnumerable()前面的結果從數據庫查詢出來,加載到內存中,然后在內存中去做分組及分頁的操作
說了這么多,貌似跟上面的查詢Group by 又有什么關系呢?為何Group by服務端會無法生成對應的sql呢?
我們仔細思考一下 GroupBy(p => p.MemberCode)返回的是什么對象呢?是IQueryable<IGrouping<TKey, TSource>>對象,而sql中 group by 查詢必須是包含在聚合函數或 GROUP BY 子句中,所以是按照sql去查詢是無法返回TSource這個對象的,這個時候程序就會需要顯示客戶端評估,才能解決
這個時候有的小伙伴靈機一動,將上面的查詢代碼改成如下:
var groupList_one = dbConContext.TMemberWelcomeLog.AsNoTracking().Where(p => p.Status == 0 && p.MerchantCode == "SH202009094127602" && p.CreateDateTime >= startTime && p.CreateDateTime <= DateTime.Now). GroupBy(p => p.MemberCode). Select(r => new { key = r.Key, count = r.Count() }); var list_one = await groupList_one.OrderByDescending(r => r.Count()).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(); var total_one = list_one.Count();
其實這樣就對了,生成的SQL如下:
SELECT [t].[MemberCode] AS [Key], COUNT(*) AS [Count] FROM[T_MemberWelcomeLog] AS[t] WHERE((([t].[Status] = 0) AND([t].[MerchantCode] = N'SH202009094127602')) AND([t].[CreateDateTime] >= @__startTime_0)) AND([t].[CreateDateTime] <= GETDATE()) GROUP BY[t].[MemberCode] ORDER BY COUNT(*) DESC OFFSET 0 ROWS FETCH NEXT @__p_1 ROWS ONLY
在官方文檔中也可以找到對應的示例【復雜查詢】
可以變換成如下方案:
var groupList_two = from p in dbConContext.TMemberWelcomeLog where p.Status == 0 && p.MerchantCode == "SH202009094127602" && p.CreateDateTime >= startTime && p.CreateDateTime <= DateTime.Now group p by p.MemberCode into g select new { g.Key, Count = g.Count() }; var list_two = groupList_two.OrderByDescending(r => r.Count).Skip((pageIndex - 1) * pageSize).Take(pageSize);
總結
在EF CORE查詢中,一定要多去想想,客戶端的方法是否真的合理嗎?這樣是否能生成對應的sql嗎?不過現在EF CORE3.0及以上版本是可以在運行的時候,拋出異常,並且在EF CORE 3.0早期版本也是可以添加警告,官方示例代碼:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFQuerying;Trusted_Connection=True;") .ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)); }