EF Core 難道不支持GroupBy嗎?


   最近在修改一個.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));
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM