n+1 problemi , db optimization
Son zamanlarda projelerimizin çoğunlukla standart CRUD operasyonlarının ötesine geçmediğini ve bu yüzden ORM (Object-Relational Mapping) araçlarına ne kadar körü körüne bağlandığımızı fark ettim.

Canlıdaki Akissy (Event Management System) uygulamam üzerinde çalışırken kendime şu soruyu sordum: "Bir veritabanı optimizasyonu yapmam gerekirse hangi adımları izlemeliyim?"
Bu düşünce beni Havelsan'daki staj dönemime götürdü. Orada Native Query'ler veya QueryDSL ile yazılmış type-safe sorgular kullanıldığını görmüştüm. O zamanlar "Zaten ORM varken neden böyle bir şeye ihtiyaç duyalım ki?" diye düşünmüştüm. Ancak bugün anlıyorum ki, sadece temel CRUD işlemleri yapmak yerine, spesifik alanlara özel ve doğru syntax ile (örneğin findEventIdByCategoryTitle gibi) hedefe yönelik sorgular atmak, büyük sistemlerde hayati önem taşıyor.
Belki monolitik uygulamalarda bu tarz ufak veritabanı gecikmeleri çok göze batmayabilir. Ancak mikroservisler gibi dağıtık sistemlerde, network iletişimini nasıl optimize ediyorsak (örneğin servisler arası iletişimde HTTP yerine gRPC kullanmak gibi), veritabanı katmanını da aynı titizlikle optimize etmeliyiz.
İşte tam bu noktada, Spring Data JPA ve Hibernate'in çalışma mekanizmalarına ve hepimizin korkulu rüyası N+1 problemine derinlemesine göz atmak istedim.
N+1 Problemi Nedir ve Neden Çözmeliyiz?
ORM'ler geliştirme hızımızı inanılmaz artırsa da, arka planda ne idüğü belirsiz onlarca SQL sorgusu üretebilir. Özellikle ilişkili tabloları (Örn: Event ve Category) çekerken, ana tablo için 1 sorgu, içindeki her bir kayıt için ilişkili tabloya N adet ekstra sorgu atılmasına N+1 problemi diyoruz.
Peki Akissy projesinde bu durumu nasıl ele alıyorum? İki temel senaryom ve iki farklı silahım var: @EntityGraph ve @BatchSize.
Senaryo 1: Kesin İhtiyaç Duyulan Detaylar ve @EntityGraph
Bir findAll metodu ile verileri getireceğimiz zaman, tüm Event'lerin içine map'lenmiş diğer verilere gerçekten gerek var mı? Listeleme veya sayfalama (pagination) yaparken genelde sadece özet bilgilere ihtiyaç duyarız.
Ancak bir Event Detail (Etkinlik Detay) sayfasına girildiğinde, o etkinliğin kategorisini, sahibini veya bilet türlerini kesinlikle göstereceğimiz bir senaryoda işler değişiyor. İşte burada imdadıma @EntityGraph yetişiyor.
@EntityGraph kullandığımızda, Hibernate arka planda bir LEFT OUTER JOIN FETCH araması yapar. Böylece ilişkili verileri tembel tembel (LAZY) sonradan çekmek yerine, tek bir veritabanı gidiş-dönüşüyle ana nesneye bağlar. Gereksiz veritabanı yükünden kurtulmuş ve gerekli bilgileri en hızlı şekilde almış olurum.
public interface EventRepository extends JpaRepository<Event, Long> {
// Sadece etkinliği değil, kategorisini de tek sorguda (JOIN) getir.
@EntityGraph(attributePaths = {"category"})
Optional<Event> findById(Long id);
}
Senaryo 2: İhtiyaç Anında Optimizasyon ve @BatchSize
Diğer bir senaryo ise verileri her zaman peşin peşin (EAGER/EntityGraph) çekmek istemediğimiz durumlar. Listeleri LAZY bırakmak istiyoruz, ancak kodun ilerleyen satırlarında bu listelere erişildiğinde de sistemin N+1 ile çökmesini istemiyoruz.
İşte burada Batch Fetching stratejisi devreye giriyor. Hibernate'e şu talimatı veriyoruz: "Bu verileri şimdi getirme ama olur da lazım olursa, teker teker değil 50'şerli gruplar halinde getir."
Bunu Spring Boot projesinde application.yml üzerinden global olarak ayarlamak oldukça basit ve inanılmaz etkilidir:
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 100
Böylece Hibernate, arka planda WHERE id IN (?, ?, ...) şeklinde tek bir sorgu atarak 100 kaydı birden getirir. N+1 problemi, sadece birkaç yığın (batch) sorgusuna dönüşür. Eğer sadece spesifik bir entity'de bunu yapmak isterseniz, koleksiyonun üzerine @BatchSize(size = 50) anotasyonunu eklemek de yeterlidir.
Sonuç
ORM araçları sihirli değnekler değildir. Özellikle mikroservis mimarilerinde her milisaniyenin önemi varken, "Nasıl olsa Spring JPA hallediyor" diyerek veritabanı erişimini kendi haline bırakamayız. Gerek @EntityGraph ile JOIN optimizasyonları yapmak, gerekse BatchSize ile tembel yüklemeleri akıllandırmak, sıradan bir uygulamayı yüksek performanslı bir sisteme dönüştüren ince detaylardır.