EF Core'da AsSplitQuery(), ilişkili koleksiyonları yükleyen bir LINQ sorgusunu tek bir JOIN'li SQL yerine birden fazla SQL sorgusuna böler. Varsayılan tek sorgu modunda aynı seviyedeki iki koleksiyonu Include etmek satırların çapraz çarpımına, yani "kartezyen patlama"ya yol açabilir; split query her dahil edilen koleksiyon için ayrı bir sorgu üreterek bu sorunu ortadan kaldırır. Bedeli ise ek veritabanı gidişleri ve sorgular arasında tutarlılık garantisinin olmamasıdır.
Entity Framework Core belgeleri bu konuda "herkese uyan tek strateji yoktur" der. Bu yazıda tek sorgunun hangi durumlarda sorun çıkardığını belgelerdeki SQL örnekleriyle gösteriyor, split query'yi sorgu ve context düzeyinde nasıl açacağınızı, EF Core'un hangi koşulda uyarı verdiğini, split query'nin tutarlılık, bellek ve sıralama tarafındaki etkilerini ve EF Core 10 ile gelen sıralama düzeltmesini anlatıyoruz. Include sözdizimi Include ile eager loading yazısında; tüm seri için EF Core öğrenme rehberi.
Tek sorgu modu ve kartezyen patlama

İlişkisel veritabanlarıyla çalışırken EF Core ilişkili varlıkları, tek bir sorguya JOIN ekleyerek yükler. JOIN SQL'in olağan bir parçasıdır, ama yanlış kullanıldığında ciddi performans sorunları doğurabilir. Belgelerdeki örnekte bir blogun hem yazıları hem de katkıda bulunanları aynı sorguda dahil edilir:
var blogs = await ctx.Blogs
.Include(b => b.Posts)
.Include(b => b.Contributors)
.ToListAsync();Belgelerin gösterdiği örnek SQL, iki koleksiyonun aynı blog tablosuna iki ayrı LEFT JOIN ile bağlandığını gösterir:
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]Posts ve Contributors aynı seviyede, ikisi de Blog'un koleksiyon navigation'ı. Bu durumda veritabanı bir çapraz çarpım döndürür: her yazı satırı her katkıcı satırıyla birleşir. Belgelerin örneğiyle, 10 yazısı ve 10 katkıcısı olan tek bir blog için veritabanı 100 satır döndürür. Aynı seviyeye her yeni koleksiyon eklediğinizde çarpan büyür ve istemciye istemeden çok büyük miktarda veri taşınabilir.
JOIN'ler aynı seviyede değilse kartezyen patlama oluşmaz. Include(b => b.Posts).ThenInclude(p => p.Comments) sorgusunda Comments, Blog'un değil Post'un koleksiyonudur; burada blogun yazıları üzerinden sahip olduğu her yorum için bir satır döner ve çapraz çarpım oluşmaz. Ayrımı akılda tutmanın kolay yolu şudur: sorun, aynı ebeveynden çıkan kardeş koleksiyonlardadır, art arda inen zincirlerde değil.
Veri tekrarı
JOIN'lerin ikinci yan etkisi veri tekrarıdır ve tek bir koleksiyon yüklerken bile ortaya çıkar. Include(b => b.Posts) sorgusunda dönen her satır hem blog hem yazı kolonlarını içerir, yani blogun kolonları her yazı için yeniden gönderilir. Çoğu zaman bu önemsizdir. Ancak blog tablosunda ikili veri ya da çok uzun metin tutan büyük bir kolon varsa (belgelerin örneğinde HugeColumn) bu kolon istemciye defalarca gider ve ağ trafiğini ciddi biçimde artırabilir. Belgelerin önerisi, büyük kolona ihtiyacınız yoksa onu hiç sorgulamamaktır:
var blogs = await ctx.Blogs
.Select(b => new
{
b.Id,
b.Name,
b.Posts
})
.ToListAsync();Projeksiyon yalnızca seçtiğiniz kolonları getirir ve belgelere göre koleksiyon yüklemeseniz bile iyi bir alışkanlıktır. Unutulmaması gereken nokta, sonucun anonim bir tür olduğu ve blogun EF tarafından takip edilmediğidir; bu nesnedeki değişiklikleri her zamanki gibi kaydedemezsiniz. Belgeler, kartezyen patlamanın aksine veri tekrarının genellikle önemli boyutlara ulaşmadığını ve ancak ana tabloda büyük kolonlar varsa dert edilmesi gerektiğini de not eder.
AsSplitQuery ile sorguyu bölmek
Bu sorunları aşmak için EF Core, bir LINQ sorgusunun birden fazla SQL sorgusuna bölünmesini belirtmenize izin verir. Split query JOIN yerine dahil edilen her koleksiyon navigation'ı için ek bir SQL sorgusu üretir:
using (var context = new BloggingContext())
{
var blogs = await context.Blogs
.Include(blog => blog.Posts)
.AsSplitQuery()
.ToListAsync();
}Belgelerdeki örnek çıktıya göre bu sorgu iki SQL komutu üretir. İlki yalnızca blogları getirir; ikincisi yazıları, blog tablosuyla INNER JOIN yaparak ve blog anahtarına göre sıralayarak getirir. EF Core iki sonucu bellekte birleştirip her blogun Posts koleksiyonunu doldurur.
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]Split query'yi varsayılan yapmak
Uygulamanızda split query'yi context için varsayılan davranış olarak ayarlayabilirsiniz. Bu ayar sağlayıcıya özgü seçenek oluşturucusunda UseQuerySplittingBehavior ile yapılır:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(
@"Server=(localdb)\mssqllocaldb;Database=EFQuerying;Trusted_Connection=True;ConnectRetryCount=0",
o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
}Split query varsayılan olduğunda belirli sorguları yine tek sorgu olarak çalıştırmak için AsSingleQuery() kullanılır. Yani iki yön de mümkündür: varsayılanı tek sorgu bırakıp gereken yerde AsSplitQuery() eklemek ya da varsayılanı split yapıp gereken yerde AsSingleQuery() eklemek. Connection string'i kod içine yazmak yerine yapılandırmadan okumak için connection string yönetimi yazısına bakın.
var blogs = await context.Blogs
.Include(blog => blog.Posts)
.AsSingleQuery()
.ToListAsync();EF Core ne zaman uyarı verir?
Herhangi bir yapılandırma yoksa EF Core tek sorgu modunu kullanır. Bu performans sorunlarına yol açabileceği için belgelere göre EF Core şu üç koşul birlikte sağlandığında bir uyarı üretir: sorgunun birden fazla koleksiyon yüklediği tespit edilmiştir, kullanıcı sorgu bölme modunu genel olarak yapılandırmamıştır ve sorguda AsSingleQuery ya da AsSplitQuery operatörü kullanılmamıştır. Uyarıyı kapatmanın yolu, bölme modunu genel olarak ya da sorgu düzeyinde uygun bir değere ayarlamaktır; böylece tercihinizi açıkça belirtmiş olursunuz.
Basit loglama belgeleri, uyarıları hataya çevirme örneğinde tam olarak bu olayı kullanır: RelationalEventId.MultipleCollectionIncludeWarning. Geliştirme ortamında bu uyarıyı istisnaya çevirmek, birden fazla koleksiyon yükleyen ve stratejisi belirtilmemiş her sorguyu erken fark etmenizi sağlar.
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.ConfigureWarnings(b => b.Throw(RelationalEventId.MultipleCollectionIncludeWarning))
.LogTo(Console.WriteLine);Split query'nin bedeli

Split query JOIN kaynaklı sorunları çözer ama kendi dezavantajlarını getirir. Belgelerin saydığı noktalar şunlardır:
- Tutarlılık: Veritabanlarının çoğu tek bir sorgu için veri tutarlılığını garanti eder, ama birden fazla sorgu için böyle bir garanti yoktur. Sorgular çalışırken veritabanı eşzamanlı olarak güncellenirse sonuç tutarsız olabilir. Belgeler bunu serializable ya da snapshot bir transaction ile sarmayı önerir, ancak bunun da kendi performans sorunlarını yaratabileceğini ekler.
- Gidiş-dönüş: Her sorgu şu anda veritabanına ek bir ağ gidişi demektir. Veritabanına gecikmenin yüksek olduğu ortamlarda (örneğin bulut servislerinde) çok sayıda gidiş performansı düşürebilir.
- Bellek: SQL Server'da MARS ve SQLite gibi bazı veritabanları birden fazla sorgunun sonucunu aynı anda okumaya izin verir; çoğu ise aynı anda yalnızca tek bir aktif sorguya izin verir. Bu durumda önceki sorguların tüm sonuçları sonraki sorgular çalışmadan önce uygulama belleğinde tamponlanmak zorundadır.
- Referans navigation'lar: Koleksiyonlarla birlikte referans navigation'lar da dahil edildiğinde, bölünen sorguların her biri bu referanslara JOIN içerir. Çok sayıda referans navigation varsa bu performansı düşürebilir.
Tutarlılık maddesi için transaction kullanımı EF Core transaction yönetimi yazısında, izolasyon seviyelerinin veritabanı tarafındaki anlamı ise transaction ve izolasyon seviyeleri yazısında anlatılıyor. Bellek maddesine bir not daha: belgelerin performans sayfası, split query kullanıldığında son sorgu dışındaki tüm sonuç kümelerinin EF tarafından içeride tamponlandığını, SQL Server'da MARS etkinse bunun olmadığını belirtir. Yeniden deneyen bir execution strategy kullanıyorsanız EF sonuçları zaten içeride tamponlar; bu tamponlama sizin ToList ile yaptığınız tamponlamaya ek olarak gerçekleşir. Execution strategy ayarları EnableRetryOnFailure ile bağlantı dayanıklılığı yazısında.
Referans navigation maddesi için geliştirme aşamasındaki EF Core 11'in sürüm notlarında bir iyileştirme yer alıyor: split query'lerde koleksiyon sorgularına eklenen gereksiz referans JOIN'lerinin budanması ve tek sorgularda referans navigation anahtarlarının ORDER BY ifadesinden çıkarılması. Bu değişiklik henüz yayımlanmış bir sürümde değil; EF Core 10 kullanıyorsanız referans JOIN'leri bölünen her sorguda görmeye devam edersiniz.
Skip/Take ile split query ve EF Core 10 düzeltmesi
Split query'lerin en sinsi sorunu sıralamayla ilgilidir. Bölünen sorgular ilgili veriyi ayrı SQL'lerle okuduğu için, her sorgunun ana kayıtları aynı sırada ve aynı kümeyle görmesi gerekir. Belgelerdeki uyarıya göre EF Core 10'dan önceki sürümlerde split query ile Skip/Take kullanırken sıralamanın tam olarak benzersiz olmasına özellikle dikkat etmek gerekiyordu; aksi hâlde yanlış veri dönebilirdi. Örneğin sonuçlar yalnızca tarihe göre sıralanıyor ve aynı tarihe sahip birden fazla kayıt varsa, bölünen sorguların her biri veritabanından farklı sonuçlar alabilirdi. Belgeler ilişkisel veritabanlarının varsayılan olarak, birincil anahtara göre bile, hiçbir sıralama uygulamadığını hatırlatır.
EF Core 10 sürüm notları bu sorunun bir parçasını düzelten değişikliği anlatır. Aşağıdaki sorguda bloglar ada göre sıralanıp ilk ikisi alınıyor ve yazıları split query ile yükleniyor:
var blogs = await context.Blogs
.AsSplitQuery()
.Include(b => b.Posts)
.OrderBy(b => b.Name)
.Take(2)
.ToListAsync();Belgelere göre EF Core 10'dan önce, yazıları okuyan ikinci sorgunun içine gömülen blog alt sorgusu yalnızca Name ile sıralanıyor, Id kolonunu atlıyordu; bu da yanlış veri dönmesine yol açabiliyordu. EF Core 10 sıralamanın sorgular arasında tutarlı olmasını sağlar. Belgelerdeki örnek çıktıda ikinci sorgunun yeni hâli şöyledir:
SELECT [p].[Id], [p].[BlogId], [p].[Title], [b0].[Id]
FROM (
SELECT TOP(@p) [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
ORDER BY [b].[Name], [b].[Id]
) AS [b0]
INNER JOIN [Post] AS [p] ON [b0].[Id] = [p].[BlogId]
ORDER BY [b0].[Name], [b0].[Id]Bu düzeltme sorgular arasındaki sıralama tutarsızlığını giderir, ama yukarıda anlatılan eşzamanlı değişiklik riskini ortadan kaldırmaz: iki SQL arasında veri değişirse sonuçlar yine birbirini tutmayabilir. Kendi sıralamalarınızı benzersiz bir anahtarla bitirmek, sayfalama yapılan her sorguda iyi bir alışkanlık olarak kalır; keyset sayfalama ve sıralama gereksinimleri sayfalama: Skip/Take ve keyset yazısında.
EF Core 11 notu: eşzamanlı değişiklikte istisna
Geliştirme aşamasındaki EF Core 11'in breaking changes sayfası bu riskle ilgili bir davranış değişikliği listeler. Bugüne kadar split query, bölünen SQL'ler arasında yapılan eşzamanlı değişiklikler nedeniyle sırası bozulmuş ya da sahipsiz kalmış alt satırlarla karşılaştığında etkilenen koleksiyonları sessizce atıyordu; sonuç, ilişkili satırları aslında var olan ama koleksiyonu boş görünen bir varlıktı ve ne istisna ne uyarı oluşuyordu. EF Core 11 ile bu durumda DbQueryConcurrencyException fırlatılması planlanıyor. Belgelerin önerdiği çözümler sorguyu yeniden çalıştırmak, sorguyu serializable ya da snapshot bir transaction içinde çalıştırmak veya her zaman tutarlı olan AsSingleQuery() moduna geçmektir. Bu davranış henüz yayımlanmış bir sürümde değildir; EF Core 10'da sessiz veri kaybı riskini sizin yönetmeniz gerekir.
Mini örnek: aynı sorgu için iki strateji

Bir raporlama ekranında her blogun yazılarını ve katkıcılarını listelediğinizi düşünün. Aşağıdaki iki sürüm aynı veriyi getirir; fark veritabanına nasıl gidildiğindedir. Kod belgelerdeki kalıplardan uyarlanmıştır ve çalıştırılmamıştır.
// 1) Tek sorgu: iki LEFT JOIN, blog başına yazı x katkıcı kadar satır
var tekSorgu = await context.Blogs
.Include(b => b.Posts)
.Include(b => b.Contributors)
.AsSingleQuery()
.ToListAsync();
// 2) Split query: blog, yazı ve katkıcı için ayrı SQL komutları
var bolunmus = await context.Blogs
.Include(b => b.Posts)
.Include(b => b.Contributors)
.AsSplitQuery()
.ToListAsync();Blog başına az sayıda yazı ve katkıcı varsa birinci sürümün getirdiği fazladan satır sayısı küçüktür ve tek gidiş avantajı öne çıkar. Blog başına yüzlerce yazı ve onlarca katkıcı varsa çapraz çarpım hızla büyür ve ikinci sürüm daha az veri taşır, ama üç ayrı gidiş yapar ve veritabanınız MARS benzeri bir özellik sunmuyorsa ilk sonuçları bellekte tamponlar. Hangisinin daha iyi olduğuna karar vermenin yolu tahmin değil ölçümdür: her iki sürümün ürettiği SQL'i loglayın, gerçek veri hacmine yakın bir ortamda süreleri karşılaştırın. Ölçüm yöntemleri EF Core performans optimizasyonu, SQL loglama ise LogTo ve interceptor yazısında.
| Ölçüt | Tek sorgu (varsayılan) | Split query |
|---|---|---|
| SQL sayısı | Bir | Her koleksiyon için ek bir sorgu |
| Kardeş koleksiyonlarda | Çapraz çarpım riski | Çapraz çarpım yok |
| Ana tablo kolonları | Her alt satırda tekrar eder | Bir kez gelir |
| Tutarlılık | Tek sorgu olarak tutarlı | Sorgular arası garanti yok |
| Ağ gidişi | Bir | Birden fazla |
| Bellek | Sonuç akış hâlinde okunabilir | Son sorgu dışındakiler tamponlanır (MARS yoksa) |
| Bire bir ilişkiler | JOIN | Yine JOIN |
Sık yapılan hatalar
- Uyarıyı görmezden gelmek: Birden fazla koleksiyon yükleyen sorgularda EF Core'un uyarısı bir tercih yapmanız gerektiğini söyler.
AsSingleQueryya daAsSplitQueryile kararınızı açıkça yazın. - Split query'yi her derde deva sanmak: Tek koleksiyonlu ve küçük sonuçlu sorgularda ek gidişler kazançtan fazlasını götürebilir.
- Sayfalamada benzersiz olmayan sıralama: Özellikle EF Core 10 öncesinde yanlış veri riski vardı; sıralamayı benzersiz bir anahtarla bitirin.
- Tutarlılığı varsaymak: Sık güncellenen tablolarda split query sonuçları birbirini tutmayabilir; gerekirse transaction kullanın ya da tek sorguya dönün.
- Büyük kolonları gereksiz yere çekmek: Veri tekrarı sorununun en kolay çözümü projeksiyondur.
Sık Sorulan Sorular
Split query varsayılan olarak açık mı?
Hayır. Herhangi bir yapılandırma yoksa EF Core tek sorgu modunu kullanır. Context düzeyinde UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) ile varsayılanı değiştirebilir ya da sorgu düzeyinde AsSplitQuery() kullanabilirsiniz.
ThenInclude zinciri de kartezyen patlamaya yol açar mı?
Belgelere göre hayır. Kartezyen patlama aynı seviyedeki kardeş koleksiyonlarda oluşur; Include(b => b.Posts).ThenInclude(p => p.Comments) gibi art arda inen bir zincirde her yorum için bir satır döner ve çapraz çarpım oluşmaz. Veri tekrarı ise yine söz konusudur.
Split query kaç SQL komutu üretir?
Ana varlık için bir sorgu ve dahil edilen her koleksiyon navigation'ı için birer ek sorgu üretir. Bire bir ilişkiler ayrı sorguya bölünmez, JOIN ile aynı sorguda kalır.
Split query sonuçlarının tutarlı olmasını nasıl sağlarım?
Belgeler sorguları serializable ya da snapshot bir transaction içinde çalıştırmayı önerir; bunun kendi performans maliyeti olabileceğini de belirtir. Kesin tutarlılık gerekiyorsa tek sorgu modu her zaman tutarlıdır.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: EF Core öğrenme rehberi · Include ile eager loading · EF Core performans optimizasyonu · EF Core transaction yönetimi · Sayfalama: Skip/Take ve keyset