1. 程式人生 > >EntityFramework Core 3多次Include導致查詢效能低之解決方案

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飆到飛起。好了,本節我們就到這裡,感謝您的閱讀,我們下節見。