單一查詢與分割查詢

單一查詢的效能問題

針對關係資料庫運作時,EF 會藉由將 JOIN 引入單一查詢來載入相關的實體。 雖然 JOIN 在使用 SQL 時相當標準,但如果使用不當,它們可能會產生顯著的效能問題。 此頁面描述這些效能問題,並顯示載入相關實體的替代方式,這些實體會加以解決。

笛卡兒爆炸

讓我們檢查下列 LINQ 查詢及其翻譯的 SQL 對等專案:

var blogs = ctx.Blogs
    .Include(b => b.Posts)
    .Include(b => b.Contributors)
    .ToList();
SELECT [b].[Id], [b].[Name], [p].[Id], [p].[BlogId], [p].[Title], [c].[Id], [c].[BlogId], [c].[FirstName], [c].[LastName]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Contributors] AS [c] ON [b].[Id] = [c].[BlogId]
ORDER BY [b].[Id], [p].[Id]

在此範例中,由於 PostsContributors 都是集合導覽 Blog ,所以兩者都位於同一 層級,關係資料庫會傳回交叉乘積:來自 Posts 的每個數據列都會與 中的每個 Contributors數據列聯結。 這表示,如果指定的部落格有 10 篇文章和 10 個參與者,資料庫就會傳回該單一部落格的 100 個數據列。 這種現象有時稱為 笛卡兒爆炸 ,可能會導致大量數據無意中傳送至用戶端,特別是當查詢中新增更多同層級的 JOIN 時,這可能會是資料庫應用程式中的主要效能問題。

請注意,當兩個 JOIN 不在相同層級時,不會發生笛卡兒爆炸:

var blogs = ctx.Blogs
    .Include(b => b.Posts)
    .ThenInclude(p => p.Comments)
    .ToList();
SELECT [b].[Id], [b].[Name], [t].[Id], [t].[BlogId], [t].[Title], [t].[Id0], [t].[Content], [t].[PostId]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Comment] AS [c] ON [p].[Id] = [c].[PostId]
ORDER BY [b].[Id], [t].[Id]

在此查詢中, Comments 是的集合導覽 Post,不同於 Contributors 先前的查詢,這是的 Blog集合導覽。 在此情況下,會針對部落格擁有的每個批注傳回單一數據列(透過其文章),而且不會發生跨產品。

資料重複

JOIN 可以建立另一種類型的效能問題。 讓我們檢查下列查詢,其中只會載入單一集合導覽:

var blogs = ctx.Blogs
    .Include(b => b.Posts)
    .ToList();
SELECT [b].[Id], [b].[Name], [b].[HugeColumn], [p].[Id], [p].[BlogId], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
ORDER BY [b].[Id]

檢查投影的數據行時,此查詢傳回的每個數據列都包含 來自和 Posts 數據表的屬性Blogs;這表示部落格屬性會針對部落格擁有的每個文章重複。 雖然這通常是正常的,而且不會造成任何問題,但如果 Blogs 數據表發生非常大的數據行(例如二進位數據或大型文字),該數據行就會重複並多次傳回用戶端。 這可大幅增加網路流量,並對您的應用程式效能造成負面影響。

如果您實際上不需要巨量數據行,就很容易就不需要查詢它:

var blogs = ctx.Blogs
    .Select(b => new
    {
        b.Id,
        b.Name,
        b.Posts
    })
    .ToList();

藉由使用投影明確選擇您想要的數據行,您可以省略大型數據行並改善效能:請注意,不論數據重複為何,這都是個好主意,因此即使未載入集合導覽,也請考慮這麼做。 不過,由於此專案會將部落格設為匿名類型,因此 EF 不會追蹤該部落格,而且無法如往常一樣儲存其變更。

值得注意的是,與笛卡兒爆炸不同,JOIN 所造成的數據重複通常並不重要,因為重複的數據大小是微不足道的:這通常只有在主體數據表中有大型數據行時,才會擔心。

分割查詢

若要解決上述的效能問題,EF 可讓您指定指定的 LINQ 查詢應該 分割 成多個 SQL 查詢。 分割查詢會針對每個包含的集合導覽產生額外的 SQL 查詢,而不是 JOIN:

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
        .AsSplitQuery()
        .ToList();
}

其會產生下列 SQL:

SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url]
FROM [Blogs] AS [b]
ORDER BY [b].[BlogId]

SELECT [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title], [b].[BlogId]
FROM [Blogs] AS [b]
INNER JOIN [Posts] AS [p] ON [b].[BlogId] = [p].[BlogId]
ORDER BY [b].[BlogId]

警告

搭配 Skip/Take 使用分割查詢時,請特別注意讓您的查詢順序完全是唯一的;這樣做可能會導致傳回不正確的數據。 例如,如果結果只依日期排序,但可能會有多個具有相同日期的結果,則每個分割查詢都可以從資料庫取得不同的結果。 依日期和標識子排序(或任何其他唯一屬性或屬性組合)的排序會讓排序完全是唯一的,並避免此問題。 請注意,關係資料庫預設不會套用任何排序,即使是在主鍵上也一樣。

注意

一對一相關實體一律會透過相同查詢中的 JOIN 載入,因為它不會影響效能。

全域啟用分割查詢

您也可以將分割查詢設定為應用程式內容的預設值:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(
            @"Server=(localdb)\mssqllocaldb;Database=EFQuerying;Trusted_Connection=True",
            o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
}

當分割查詢設定為預設值時,仍然可以將特定查詢設定為以單一查詢的形式執行:

using (var context = new SplitQueriesBloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
        .AsSingleQuery()
        .ToList();
}

EF Core 預設會在沒有任何設定的情況下使用單一查詢模式。 因為可能會造成效能問題,所以 EF Core 會在符合下列條件時產生警告:

  • EF Core 會偵測查詢載入多個集合。
  • 使用者尚未全域設定查詢分割模式。
  • 使用者尚未在 AsSingleQuery/AsSplitQuery 查詢上使用 運算符。

若要關閉警告,請將全域或查詢層級的查詢分割模式設定為適當的值。

分割查詢的特性

雖然分割查詢可避免與 JOIN 和笛卡兒爆炸相關的效能問題,但也有一些缺點:

  • 雖然大部分的資料庫都保證單一查詢的數據一致性,但多個查詢沒有這類保證。 如果在執行查詢時同時更新資料庫,則產生的數據可能不一致。 您可以藉由將查詢包裝在可串行化或快照集交易中來緩和它,不過這樣做可能會自行產生效能問題。 如需詳細資訊,請參閱資料庫的檔。
  • 每個查詢目前都表示您的資料庫有額外的網路往返。 多個網路往返可能會降低效能,特別是在資料庫延遲很高時(例如雲端服務)。
  • 雖然某些資料庫允許同時取用多個查詢的結果(SQL Server 搭配MARS、Sqlite),但大部分資料庫只允許在任何指定時間點使用單一查詢。 因此,先前查詢的所有結果都必須在應用程式的記憶體中緩衝處理,再執行稍後的查詢,這會導致記憶體需求增加。
  • 包含參考導覽以及集合導覽時,每個分割查詢都會包含參考導覽的聯結。 這可能會降低效能,特別是如果有許多參考流覽。 如果這是您想要查看的修正專案,請提出 #29182

不幸的是,載入符合所有案例的相關實體沒有一種策略。 請仔細考慮單一查詢和分割查詢的優點和缺點,以選取符合您需求的查詢。