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 Posts
Contributors
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 Contributors
her 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 Post
olan ö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ı
10'undan önceki EF sürümlerinde Skip/Take ile bölünmüş sorgular kullanırken, sorgu sıralamanızı tamamen benzersiz hale getirmek için ö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;ConnectRetryCount=0",
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.