JPA N+1 Problemi
Önce şunu oku: IoC ve Dependency Injection
30 saniyede özet
Bir liste çektin, sonra her elemanın detayına baktın: 1 sorgu yerine 1+N sorgu çalıştı. Sorun sorguların yavaşlığı değil, sayısı. EAGER bunu çözmez; fetch join, EntityGraph ya da BatchSize çözer.
Markete bir kere gidip her şeyi almak yerine her ürün için ayrı gittiğini düşün. Yol kısa olsa bile akşam olur.
Adım adım oku
- Önce siparişlerin listesi çekilir: bir sorgu.
- Sonra her siparişin kalemlerine bakılırken, her biri için ayrı bir sorgu daha gider. Beş sipariş için toplam altı tur.
- JOIN FETCH ile siparişler ve kalemleri tek sorguda, tek turda getirilir.
- Sorun tek bir sorgunun yavaşlığı değil, tur sayısıdır. Sipariş sayısı arttıkça tur sayısı da artar.
-
Bayt: Siparişleri listeleyip her birinin kalemlerini saydım. Tek satır kod!
-
Sen: Log'larda kaç sorgu görünüyor peki?
-
Bayt: Yüzlerce! Her sipariş için bir tane daha.
-
Bayt: Markete her ürün için ayrı gitmek gibi. Sorun yol değil, gidiş sayısı.
Bu kod bir sorgu gibi görünür ama altı sorgu çalıştırır:
List<Order> orders = orderRepository.findAll(); // 1 sorgu
for (Order order : orders) { total += order.getLineItems().size(); // her sipariş için 1 sorgu daha}Beş sipariş varsa 6 sorgu. Beş bin sipariş varsa 5001. Buna N+1Bir sorgu listeyi getirir, sonra her eleman için bir sorgu daha atılır. Her biri hızlı olduğu için yavaş sorgu logunda görünmez; yavaşlayan şey toplam gidiş-dönüş sayısıdır.Sözlükte gör → problemi denir.
Sorun hız değil, sayı
Her sorgu tek başına milisaniyeler sürer ve veritabanı logunda hiçbiri yavaş görünmez. Maliyet sayıdadır: her sorgu bir ağ gidiş dönüşü, bir bağlantı kullanımı ve bir tur parse/plan işi demektir.
Bu yüzden N+1 üretimde geç fark edilir — profiler “yavaş sorgu” göstermez, sadece uygulama yavaştır.
Kafam karıştı, daha basit anlat
Adı buradan geliyor: 1 sorgu siparişleri getirir, sonra N sipariş için N ayrı sorgu daha gider. Markete her ürün için ayrı ayrı gitmek gibi: her yol kısa, ama toplamı uzun.
N+1 sorgu problemi tam olarak nedir?
Kendin gör
N+1 sorgu problemi ve dört çözüm denemesi
Tohum 3Siparişler · her birinde 3 kalem
Çalışan SQL
henüz sorgu yok
Şu an ne oldu?
Kök sorgu çalışacak
Önce siparişler yüklenir. Koleksiyonların ne zaman ve kaç sorguyla geleceğini seçtiğin strateji belirler.
Aklında kalsın: Strateji entity üzerinde değil, sorgu başına seçilmelidir. Aynı entity bir ekranda koleksiyonsuz, başka bir ekranda koleksiyonla gerekebilir.
Görevler0/3
En az 10 siparişte N+1’i gör: 1 + N sorguaçık
İpucu
LAZY ile sipariş sayısını 10’un üstüne çıkar ve sonuna kadar oynat. Sorgu listesini say.
Aynı işi, koleksiyona dokunarak ve uyarısız, tek sorguyla yapaçık
İpucu
JOIN FETCH ya da @EntityGraph dene. Koleksiyona dokunmayı kapatmak hile sayılmaz.
JOIN FETCH ile sayfalamanın tuzağını tetikleaçık
İpucu
JOIN FETCH seçiliyken sayfalamayı aç. Tek sorgu çıkıyor ama LIMIT nerede uygulanıyor?
Olay günlüğü (0)
Henüz olay yok. Oynat veya adımla.
Sırayla dene:
LAZY(varsayılan) — kök sorgu + sipariş başına bir sorgu. Sorgu sayacına bak.Kod order.getLineItems() çağırıyorkutusunu kapat. Tek sorgu. LAZY’nin kendisi sorun değil; sorun ona döngü içinde erişmek.EAGERyap ve kutuyu tekrar kapat. Sorgu sayısı yine 1 + N. Koleksiyona hiç dokunmasan bile. Bu, dersin en önemli gözlemi.JOIN FETCH— tek sorgu, amaÇekilen satırsayacı zıplıyor: kartezyen çarpım.@BatchSize— sorgu sayısı 1 + tavan(N/batch). Kartezyen çarpım yok.JOIN FETCH+Sayfalamabirlikte — kırmızıHHH000104uyarısı.
Simülatörün altındaki Görevler listesi bu gözlemleri üç hedefe topluyor. İkinci görevde koleksiyona dokunmayı kapatmak kabul edilmiyor, çünkü gerçek kod o veriye ihtiyaç duyuyor.
Kafam karıştı, daha basit anlat
Simülatörde sorgu sayısına bak, sürelere değil. Tek tek hızlı görünen sorgular, sayıları artınca uygulamayı yavaşlatır.
Bu kod kaç SQL sorgusu üretir? (10 sipariş var, her birinin bir müşterisi)
EAGER neden çözmez
100 sipariş listelenirken her siparişin kalemleri ayrı bir sorguyla geliyor: 101 sorgu. lineItems ilişkisini EAGER yaptın. Sorgu sayısı kaça iner? Cevabı göster
İnmez. Koleksiyonlar için Hibernate yine sipariş başına bir SELECT çalıştırır; yalnızca bunu sen istemeden ve her seferinde yapar.
En yaygın yanlış refleks fetch = FetchType.EAGER yazmaktır. Koleksiyonlar için Hibernate yine parent başına ayrı bir SELECT çalıştırır; sadece bunu sen istemeden yapar.
@OneToMany(mappedBy = "order", fetch = FetchType.EAGER) // hâlâ 1 + Nprivate List<LineItem> lineItems;Üstelik durumu kötüleştirir: artık koleksiyona ihtiyaç duymayan ekranlarda bile yükleniyor ve bundan kaçınmanın bir yolu yok. EAGER bir optimizasyon değil, bir seçeneği elinden almaktır.
Aşağıdaki örnek bir bankanın kredi kartı ekstre ekranından. N+1’in üç çözümü bir arada: fetch join, entity graph, sayfalama için iki adımlı sorgu ve sorgu sayısını ölçen bir test.
Derinleş · Kredi kartı ekstreleri: N+1'siz, sayfalı ve test edilmiş 5 dosya · ~109 satır · ilk okumada atlayabilirsin
`LAZY` ve `EAGER` fetch arasındaki fark nedir?
N+1'i çözmek için `@OneToMany(fetch = EAGER)` yapmak neden kötü bir fikirdir?
Üç gerçek çözüm
1. JOIN FETCH — tek sorgu, açık ve net:
@Query("select distinct o from Order o join fetch o.lineItems")List<Order> findAllWithItems();2. @EntityGraph — aynı şey, JPQL yazmadan:
@EntityGraph(attributePaths = "lineItems")List<Order> findAll();3. @BatchSize — sorguları gruplar:
@OneToMany(mappedBy = "order")@BatchSize(size = 25) // 1 + tavan(N/25) sorgu, kartezyen çarpım yokprivate List<LineItem> lineItems;hibernate.default_batch_fetch_size ile bunu uygulama genelinde varsayılan yapabilirsin — tek satırlık, düşük riskli ve çoğu projede en iyi ilk hamledir.
JOIN FETCH’in iki tuzağı. Kartezyen çarpım. Her sipariş, kalem sayısı kadar satırda tekrarlanır. İki koleksiyonu aynı anda fetch etmeye çalışırsan Hibernate durur:
MultipleBagFetchException: cannot simultaneously fetch multiple bagsÇözüm: List yerine Set kullanmak, ya da koleksiyonları ayrı sorgulara bölmek.
Sayfalama ile birlikte çalışmaz. Üretimde en çok şaşırtan detay bu:
HHH000104: firstResult/maxResults specified with collection fetch; applying in memoryHibernate bir koleksiyon fetch’ine LIMIT uygulayamaz — satır sınırı koleksiyonun ortasını keserdi. Bunun yerine tüm tabloyu çeker ve sayfalamayı bellekte yapar. Küçük veride fark edilmez, büyük veride uygulamayı düşürür.
Doğru çözüm iki adımlıdır:
// 1. Yalnızca id'leri sayfalayarak çek — koleksiyon yok, LIMIT çalışırPage<Long> ids = orderRepository.findIdsBy(pageable);
// 2. O id'ler için tek bir fetch joinList<Order> orders = orderRepository.findWithItemsByIdIn(ids.getContent());Kafam karıştı, daha basit anlat
Çözüm, alışveriş listesini tek seferde vermektir. join fetch siparişleri ve satırlarını tek bir sorguda getirir.
Her yaklaşımı tek sorgu üretip üretmediğine göre yerleştir.
Tuzaklar ve seçim
| Durum | Çözüm |
|---|---|
| Tek koleksiyon, sayfalama yok | JOIN FETCH veya @EntityGraph |
| Birden fazla koleksiyon | @BatchSize (kartezyen çarpımdan kaçın) |
| Sayfalama gerekli | İki adımlı: önce id’ler, sonra fetch |
| Yalnızca birkaç alan gerekli | DTO projeksiyonu — entity hiç yükleme |
| Genel güvenlik ağı | hibernate.default_batch_fetch_size |
Son satır özellikle değerli: DTO projeksiyonu yalnızca ihtiyacın olan sütunları çeker, persistence contextHibernate'in yönettiği entity'leri tuttuğu birinci seviye önbellek. Transaction ile birlikte açılır ve kapanır; kapandıktan sonra lazy ilişkilere dokunmak hata verir.Sözlükte gör →’i kirletmez ve dirty checking maliyeti oluşturmaz.
Kendini sına
N+1 sorunu, liste için bir sorgu ve her eleman için birer sorgu daha çalışmasıdır.
Bir `@OneToMany` koleksiyonunu `FetchType.EAGER` yaparsanız N+1 problemi çözülür mü?
Aklında kalacak üç şey
- 1 EAGER bir çözüm değildir: koleksiyonlarda EAGER de sipariş başına bir SELECT çalıştırır. Sorgu sayısı LAZY ile aynıdır, tek fark kaçamamandır.
- 2 Sorgular yavaş değil, sayıları fazla. Her biri veritabanına ayrı bir gidiş dönüştür.
- 3 Sayfalama ile JOIN FETCH birlikte çalışmaz: Hibernate sınırı veritabanında uygulayamaz, bütün tabloyu belleğe çekip orada sayfalar.
5 kart sonraki derste seni bekliyor
Bu dersin üstüne kurulanlar
Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.
- Spring Boot EkosistemiPersistence Context — Hibernate'in Hafızasısave() çağırmadın ama değişiklik yine de veritabanına yazıldı. Kim kaydetti?Derse git
- Spring Boot EkosistemiJPA Projection ve DTO — Üç Alan İçin Ne Kadar Veri?Listede yalnızca isim ve fiyat göstereceksen bütün nesneyi yüklemek neden pahalı?Derse git