EntityFramework Core 3多次Include導致查詢效能低之解決方案
前言
上述我們簡單講解了幾個小問題,這節我們再來看看如標題EF Core中多次Include導致出現效能的問題,廢話少說,直接開門見山。
EntityFramework Core 3多次Include查詢問題
不要嫌棄我囉嗦,我們凡事從頭開始講解起,首先依然給出我們上一節的示例類:
public class EFCoreDbContext : DbContext { public EFCoreDbContext() { } public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseSqlServer(@"Server=.;Database=EFTest;Trusted_Connection=True;"); } public class Blog { public int Id { get; set; } public string Name { get; set; } public List<Post> Posts { get; set; } } public class Post { public int Id { get; set; } public int BlogId { get; set; } public string Title { get; set; } public string Content { get; set; } public Blog Blog { get; set; } }
接下來我們在控制檯進行如下查詢:
var context = new EFCoreDbContext(); var blog = context.Blogs.FirstOrDefault(d => d.Id == 1);
如上圖所示,生成的SQL語句一點毛病都麼有,對吧,接下來我們來查詢導航屬性Posts,如下:
var context = new EFCoreDbContext(); var blog = context.Blogs.AsNoTracking() .Include(d => d.Posts).FirstOrDefault(d => d.Id == 1);
咦,不應該是INNER JOIN嗎,但最終生成的SQL語句我們可以看到居然是LEFT JOIN,關鍵是我們對Post類中的BlogId並未設定為可空,對吧,是不是很有意思。同時通過ORDER BY對兩個表的主鍵都進行了排序。這就是問題的引發點,接下來我們再引入兩個類:
/// <summary> /// 部落格標籤 /// </summary> public class Tag { public int Id { get; set; } /// <summary> /// 標籤名稱 /// </summary> public string Name { get; set; } public int BlogId { get; set; } public Blog Blog { get; set; } } /// <summary> /// 部落格分類 /// </summary> public class Category { /// <summary> /// /// </summary> public int Id { get; set; } /// <summary> /// 分類名稱 /// </summary> public string Name { get; set; } /// <summary> /// /// </summary> public int BlogId { get; set; } /// <summary> /// /// </summary> public Blog Blog { get; set; } }
上述我們聲明瞭分類和標籤,我們知道部落格有分類和標籤,所以部落格類中有對分類和標籤的導航屬性(這裡我們先不關心關係到底是一對一還是一對多等關係),然後修改部落格類,如下:
public class Blog { public int Id { get; set; } public string Name { get; set; } public List<Post> Posts { get; set; } public List<Tag> Tags { get; set; } public List<Category> Categories { get; set; } }
接下來我們再來進行如下查詢:
var context = new EFCoreDbContext(); var blogs = context.Blogs.AsNoTracking().Include(d => d.Posts) .Include(d => d.Tags) .Include(d => d.Categories).FirstOrDefault(d => d.Id == 1);
SELECT [t].[Id], [t].[Name], [p].[Id], [p].[BlogId], [p].[Content], [p].[Title], [t0].[Id], [t0].[BlogId], [t0].[Name], [c].[Id], [c].[BlogId], [c].[Name] FROM ( SELECT TOP(1) [b].[Id], [b].[Name] FROM [Blogs] AS [b] WHERE [b].[Id] = 1 ) AS [t] LEFT JOIN [Posts] AS [p] ON [t].[Id] = [p].[BlogId] LEFT JOIN [Tags] AS [t0] ON [t].[Id] = [t0].[BlogId] LEFT JOIN [Categories] AS [c] ON [t].[Id] = [c].[BlogId] ORDER BY [t].[Id], [p].[Id], [t0].[Id], [c].[Id]
此時和變更追蹤沒有半毛錢關係,我們看看最終生成的SQL語句,是不是很驚訝,假設單個類中對應多個導航屬性,最終生成的SQL語句就是繼續LEFT JOIN和ORDER BY,可想其效能將是多麼的低下。那麼我們應該如何解決這樣的問題呢?既然是和Include有關係,每增加一個導航屬性即增加一個Include將會增加一個LEFT JOIN和ORDER BY,那麼我們何不分開單獨查詢呢,說完就開幹。
var context = new EFCoreDbContext(); var blog = context.Blogs.AsNoTracking().FirstOrDefault(d => d.Id == 1);
此時我們進行如上查詢顯然不可取,因為直接就到資料庫進行SQL查詢了,我們需要返回IQueryable才行,同時根據主鍵查詢只能返回一條,所以我們改造成如下查詢:
var context = new EFCoreDbContext(); var blog = context.Blogs.Where(d => d.Id == 1).Take(1);
因為接下來還需要從上下文中載入導航屬性,所以這裡我們需要去掉AsNoTracking,通過上下文載入指定實體導航屬性,我們可通過Load方法來載入,如下:
var context = new EFCoreDbContext(); var blog = context.Blogs.Where(d => d.Id == 1).Take(1); blog.Include(p => p.Posts).SelectMany(d => d.Posts).Load(); blog.Include(t => t.Tags).SelectMany(d => d.Tags).Load(); blog.Include(c => c.Categories).SelectMany(d => d.Categories).Load();
SELECT [p].[Id], [p].[BlogId], [p].[Content], [p].[Title] FROM ( SELECT TOP(1) [b].[Id], [b].[Name] FROM [Blogs] AS [b] WHERE [b].[Id] = 1 ) AS [t] INNER JOIN [Posts] AS [p] ON [t].[Id] = [p].[BlogId] SELECT [t0].[Id], [t0].[BlogId], [t0].[Name] FROM ( SELECT TOP(1) [b].[Id], [b].[Name] FROM [Blogs] AS [b] WHERE [b].[Id] = 1 ) AS [t] INNER JOIN [Tags] AS [t0] ON [t].[Id] = [t0].[BlogId] SELECT [c].[Id], [c].[BlogId], [c].[Name] FROM ( SELECT TOP(1) [b].[Id], [b].[Name] FROM [Blogs] AS [b] WHERE [b].[Id] = 1 ) AS [t] INNER JOIN [Categories] AS [c] ON [t].[Id] = [c].[BlogId]
通過上述生成的SQL語句,我們知道這才是我們想要的結果,上述程式碼看起來有點不是那麼好看,似乎沒有更加優美的寫法了,當然這裡我只是在控制檯中進行演示,為了吞吐,將上述修改為非同步查詢則是最佳可行方式。 比生成一大堆LEFT JOIN和ORDER BY效能好太多太多。
總結
注意:上述博主採用的是穩定版本3.0.1,其他版本未經測試哦。其實對於查詢而言,還是建議採用Dapper或者走底層connection寫原生SQL才是最佳,對於單表,用EF Core無可厚非,對於複雜查詢還是建議不要用EF Core,生成的SQL很不可控,為了圖方便,結果換來的將是CPU飆到飛起。好了,本節我們就到這裡,感謝您的閱讀,我們下節見。