Tek ve Bölünmüş Sorgular karşılaştırması

Tek sorgularla ilgili performans sorunları

İlişkisel veritabanlarında çalışırken EF, JOIN'leri tek bir sorguya ekleyerek ilgili varlıkları yükler. SQL kullanılırken JOIN'ler oldukça standart olsa da, yanlış kullanıldığında önemli performans sorunları oluşturabilir. Bu sayfada bu performans sorunları açıklanır ve bunların etrafında çalışan ilgili varlıkları yüklemek için alternatif bir yol gösterilir.

Kartezyen patlaması

Şimdi aşağıdaki LINQ sorgusunu ve çevrilmiş SQL eşdeğerini inceleyelim:

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]

Bu örnekte, hem hem de PostsContributors koleksiyon gezintileri Blog olduğundan ( aynı düzeyde olduklarından) ilişkisel veritabanları çapraz bir ürün döndürür: içindeki her satır Posts , içindeki Contributorsher satır ile birleştirilir. Bu, belirli bir blogda 10 gönderi ve 10 katkıda bulunan varsa veritabanının bu tek blog için 100 satır döndürdüğü anlamına gelir. Bazen kartezyen patlama olarak da adlandırılan bu fenomen, özellikle sorguya daha fazla eşdüzey JOIN eklendikçe çok miktarda verinin istemeden istemciye aktarılmasına neden olabilir; bu, veritabanı uygulamalarında önemli bir performans sorunu olabilir.

İki JOIN aynı düzeyde olmadığında kartezyen patlamanın gerçekleşmediğini unutmayın:

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]

Bu sorguda, Comments öğesinin koleksiyon gezintisi Postolan önceki sorgunun aksine Contributors bir koleksiyon gezintisidir Blog. Bu durumda, blogda bulunan her yorum için tek bir satır döndürülür (gönderileri aracılığıyla) ve çapraz ürün gerçekleşmez.

Veri yineleme

JOIN'ler başka bir performans sorunu oluşturabilir. Şimdi yalnızca tek bir koleksiyon gezintisini yükleyen aşağıdaki sorguyu inceleyelim:

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]

Öngörülen sütunlarda incelenirken, bu sorgu tarafından döndürülen her satır hem hem Posts de Blogs tablolarından özellikler içerir; bu, blog özelliklerinin blogda bulunan her gönderi için yinelendiği anlamına gelir. Bu genellikle normal olsa da ve hiçbir soruna neden olmasa da, tabloda çok büyük bir sütun (örneğin ikili veriler veya çok büyük bir metin) varsa Blogs , bu sütun çoğaltılır ve istemciye birden çok kez geri gönderilir. Bu, ağ trafiğini önemli ölçüde artırabilir ve uygulamanızın performansını olumsuz etkileyebilir.

Çok büyük bir sütuna ihtiyacınız yoksa kolayca sorgulayamazsınız:

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

İstediğiniz sütunları açıkça seçmek için bir projeksiyon kullanarak büyük sütunları atlayabilir ve performansı geliştirebilirsiniz; Veri yinelemeden bağımsız olarak bunun iyi bir fikir olduğunu unutmayın, bu nedenle koleksiyon gezintisi yüklemediğinde bile bunu yapmayı göz önünde bulundurun. Ancak, bu blogu anonim bir türe projelediğinden, blog EF tarafından izlenmez ve değişiklikleri her zamanki gibi geri kaydedilemez.

Kartezyen patlamanın aksine, yinelenen veri boyutu göz ardı edilebilir olduğundan JOIN'lerin neden olduğu veri yinelemesinin genellikle önemli olmadığını belirtmek gerekir; Bu genellikle yalnızca asıl tablonuzda büyük sütunlar varsa endişelenecek bir şeydir.

Sorguları bölme

Yukarıda açıklanan performans sorunlarına geçici bir çözüm bulmak için EF, belirli bir LINQ sorgusunun birden çok SQL sorgusuna bölünmesi gerektiğini belirtmenize olanak tanır. JoIN'ler yerine bölünmüş sorgular, eklenen her koleksiyon gezintisi için ek bir SQL sorgusu oluşturur:

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

Aşağıdaki SQL'i oluşturur:

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]

Uyarı

Atlama/Alma ile bölünmüş sorgular kullanırken, sorgu sıralamanızın tamamen benzersiz hale getirilmesine özellikle dikkat edin; bunun yapılmaması yanlış verilerin döndürülmesine neden olabilir. Örneğin, sonuçlar yalnızca tarihe göre sıralanıyorsa ancak aynı tarihe sahip birden çok sonuç olabilirse, bölünmüş sorguların her biri veritabanından farklı sonuçlar alabilir. Hem tarihe hem de kimliğe göre (veya başka bir benzersiz özelliğe veya özellik birleşimine göre) sıralama tamamen benzersiz hale gelir ve bu sorundan kaçınır. İlişkisel veritabanlarının, birincil anahtara bile varsayılan olarak herhangi bir sıralama uygulamadığını unutmayın.

Not

Bire bir ilgili varlıklar her zaman aynı sorgudaki JOIN'ler aracılığıyla yüklenir, bu nedenle performans etkisi yoktur.

Bölünmüş sorguları genel olarak etkinleştirme

Ayrıca, bölünmüş sorguları uygulamanızın bağlamı için varsayılan olarak yapılandırabilirsiniz:

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

Bölünmüş sorgular varsayılan olarak yapılandırıldığında, belirli sorguları tek sorgu olarak yürütülecek şekilde yapılandırmak mümkündür:

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

EF Core, yapılandırma olmadığında varsayılan olarak tek sorgu modunu kullanır. Performans sorunlarına neden olabileceğinden EF Core, aşağıdaki koşullar karşılandığında bir uyarı oluşturur:

  • EF Core, sorgunun birden çok koleksiyon yüklediğini algılar.
  • Kullanıcı sorgu bölme modunu genel olarak yapılandırmadı.
  • Kullanıcı sorguda işleci kullanmadı AsSingleQuery/AsSplitQuery .

Uyarıyı kapatmak için, sorgu bölme modunu genel olarak veya sorgu düzeyinde uygun bir değerle yapılandırın.

Bölünmüş sorguların özellikleri

Bölünmüş sorgu JOIN'ler ve kartezyen patlamayla ilgili performans sorunlarını önlerken bazı dezavantajları da vardır:

  • Veritabanlarının çoğu tek sorgular için veri tutarlılığını garanti ederken, birden çok sorgu için böyle bir garanti yoktur. Sorgularınızı yürütürken veritabanı eşzamanlı olarak güncelleştiriliyorsa sonuçta elde edilen veriler tutarlı olmayabilir. Sorguları seri hale getirilebilir veya anlık görüntü işlemine sarmalayarak bunu azaltabilirsiniz, ancak bunu yapmak kendi performans sorunları oluşturabilir. Daha fazla bilgi için veritabanınızın belgelerine bakın.
  • Her sorgu şu anda veritabanınıza ek bir ağ gidiş dönüşleri anlamına gelir. Özellikle veritabanında gecikme süresinin yüksek olduğu durumlarda (örneğin bulut hizmetleri) birden çok ağ gidiş dönüşleri performansı düşürebilir.
  • Bazı veritabanları birden çok sorgunun sonuçlarının aynı anda (MARS, Sqlite ile SQL Server) tüketilmesine izin verirken, çoğu belirli bir noktada yalnızca tek bir sorgunun etkin olmasına izin verir. Bu nedenle önceki sorgulardan elde edilen tüm sonuçlar, daha sonraki sorguları yürütmeden önce uygulamanızın belleğinde arabelleğe alınmalıdır ve bu da bellek gereksinimlerini artırır.
  • Başvuru gezintilerinin yanı sıra koleksiyon gezintilerini de eklerken, bölünmüş sorguların her biri başvuru gezintilerine katılmaları içerir. Bu, özellikle de çok sayıda başvuru gezintisi varsa performansı düşürebilir. Bu sorunu çözmek istiyorsanız lütfen #29182'yi destekleyin.

Ne yazık ki, tüm senaryolara uyan ilgili varlıkları yüklemek için tek bir strateji yoktur. İhtiyaçlarınıza uygun olanı seçmek için tek ve bölünmüş sorguların avantajlarını ve dezavantajlarını dikkatlice göz önünde bulundurun.