iToverDose/Yazılım· 23 MAYIS 2026 · 12:05

Django REST API'de N+1 Sorununu Üç Satırda Nasıl Çözdüm?

Django REST API'deki sessiz performans katillerinden N+1 sorgusunu gerçek bir örnekle keşfedin. Üç satırlık basit bir düzeltmeyle uygulamanızın yanıt sürelerini nasıl optimize edebileceğinizi öğrenin.

DEV Community3 dk okuma0 Yorumlar

Veri tabanı sorgularının performansınızı yavaşlattığını asla fark etmeyebilirsiniz — ta ki üretim ortamında aniden yavaşlamaya başlayana kadar. Django REST Framework kullanarak geliştirilen bir API'de karşılaşılan en yaygın gizli performans sorunlarından biri olan N+1 sorgusu, sessizce veri seti büyüdükçe her istekle birlikte gelen cevap süresini artırır. Bugün yaşadığım deneyimden yola çıkarak, bu sorunu nasıl tespit ettiğimi ve sadece üç satırlık bir düzeltmeyle nasıl çözdüğümü adım adım paylaşacağım.

N+1 Sorgusu Nedir ve Neden Tehlikelidir?

N+1 sorgusu, temel olarak bir API endpoint'inin birden fazla ilişkisel veri çekerken yaptığı verimsiz sorgulama modelini tanımlar. Bu durumda, N adet kayıt çekmek için başlangıçta sadece 1 sorgunun yapılması gerekirken, ilişkili her kayıt için 1 ek sorgunun çalıştırılması sonucu toplamda N+1 adet sorgunun gerçekleşmesine neden olur.

Django'nun ORM yapısı gereği varsayılan olarak tembel (lazy) çalışması nedeniyle, ilişkili bir alana erişilmeden önce bu verilerin önceden yüklenmemiş olması durumunda her erişimde yeni bir sorgunun tetiklendiği unutulmamalıdır. Örneğin, blog gönderilerini listeleyen bir endpoint'te her gönderinin altında bulunan dizi (series) ve etiket (tag) bilgileri, ilişkisel tablolardan ayrı sorgularla çekilmeye başlanır. 30 gönderilik bir listede bu durum, sadece 30 ek sorgunun değil, toplamda 61 sorgunun çalıştırılmasına yol açar — ki bu da yanıt süresini ciddi şekilde artırır.

Hatanın Kaynağı: Saklı N+1 Sorgusu

Uygulamamda /api/blog-posts/ endpoint'inde performans düşüşü yaşadığımı Sentry'den aldığım uyarılar sayesinde fark ettim. Görünüşte temiz görünen BlogPostViewSet ve BlogPostSerializer sınıfları, ilişkisel alanlara erişim sırasında sessizce sorguların artmasına neden oluyordu.

class BlogPostViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = BlogPost.objects.all()
    serializer_class = BlogPostSerializer
    lookup_field = "uid"

class BlogPostSerializer(serializers.ModelSerializer):
    tags = BlogTagSerializer(many=True, read_only=True)
    series = BlogSeriesSerializer(read_only=True)
    ...

Yukarıdaki kodda BlogPost modelinin, series adında bir ForeignKey ve tags adında bir ManyToManyField ilişkisine sahip olduğunu görüyoruz. DRF tarafından bir gönderi listesinin serileştirilmesi sırasında her gönderi için bu ilişkisel alanlara erişilmesi gerekiyor. Eğer sorgular önceden optimize edilmezse, her gönderi için birer sorgunun tetiklenmesi kaçınılmaz oluyor. Bu durumda, 30 gönderi için toplamda 61 sorgunun çalıştırılması anlamına geliyor.

Benzer bir sorun, featured adındaki özel eylemde de mevcuttu:

@action(detail=False, methods=["get"])
def featured(self, request):
    queryset = BlogPost.objects.filter(
        date_published__isnull=False
    ).order_by("-date_published")[:3]

Bu fonksiyonda da ilişkisel alanların önceden yüklenmemesi nedeniyle her üç gönderi için ayrı sorguların tetiklenmesine yol açıyordu.

N+1 Sorgusunun Üç Satırlık Çözümü

Django ORM, ilişkisel sorguları optimize etmek için iki önemli yöntem sunar:

  • `select_related()`: ForeignKey ve OneToOne ilişkileri için kullanılır. SQL düzeyinde JOIN işlemi gerçekleştirerek tüm ilişkili verileri tek bir sorguda çeker.
  • `prefetch_related()`: ManyToManyField ve ters yönlü ForeignKey ilişkileri için kullanılır. İkinci bir sorgunun çalıştırılmasını sağlayarak ilişkili verilerin Python tarafında önbelleklenmesini sağlar.

Bu yöntemleri kullanarak yaptığım düzeltme şu şekildeydi:

class BlogPostViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = BlogPost.objects.select_related("series").prefetch_related("tags")
    serializer_class = BlogPostSerializer
    lookup_field = "uid"

    @action(detail=False, methods=["get"])
    def featured(self, request):
        queryset = (
            BlogPost.objects
            .select_related("series")
            .prefetch_related("tags")
            .filter(date_published__isnull=False)
            .order_by("-date_published")[:3]
        )
        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)

Bu değişiklikle birlikte artık 30 gönderilik bir liste için sadece 3 sorgunun çalıştırılması sağlandı:

  • Tüm blog gönderilerini çeken ana sorgu
  • series ilişkisini tek bir JOIN sorgusuyla çeken sorgu
  • tags ilişkisini önbellekleyen ikinci sorgu

Ekstra Bir N+1 Sorunu: TestimonialViewSet

Blog gönderileriyle ilgili iyileştirmeleri yaparken, aynı N+1 probleminin TestimonialViewSet sınıfında da olduğunu fark ettim. Bu sınıfta, project.title ve project.slug alanlarına erişilirken sorguların optimize edilmediği görülüyordu. Basit bir düzeltmeyle sorun çözüldü:

# Önce
queryset = Testimonial.objects.all()

# Sonra
queryset = Testimonial.objects.select_related("project")

Sadece bir satırlık bir değişiklikle, bu endpoint'te de N+1 sorgusu ortadan kalktı.

Gelecekte N+1 Sorgularını Nasıl Önlersiniz?

N+1 sorgularını kod inceleme aşamasında yakalamak, üretim ortamında karşılaşılan performans sorunlarını önlemenin en etkili yoludur. Bu sorunları tespit etmek için kullanılabilecek bazı araçlar şunlardır:

  • django-debug-toolbar: Tarayıcıda her istek için yapılan sorguları göstererek performans analizi yapmayı sağlar.
  • nplusone: Testler sırasında N+1 sorgularını tespit eden ve hata fırlatan bir kütüphanedir.
  • Sentry Performance: Üretim ortamında sorguları izleyerek performans düşüşlerini erken tespit eder.

Herhangi bir ViewSet veya görünüm yazarken ilişkisel alanlara erişilip erişilmediğini sorgulamak önemlidir. Örneğin, seri hale getiricilerin (serializer) ilişkisel bir alana erişip erişmediği kontrol edilmelidir. Bunun için şu soruları kendinize sormalısınız:

  • Bu görünümün sorgusu, ilişkisel alanları önceden yükleyecek şekilde optimize edilmiş mi?
  • Eğer seri hale getiricide source="relation.field" kullanılıyorsa, ilişkisel veriler önceden yükleniyor mu?

Sonuç: Performans İçin Basit Bir Kontrol Listesi

Django'nun tembel değerlendirme özelliği, kodunuzu daha okunabilir ve modüler hale getirirken, ilişkisel sorguların optimize edilmesini gerektirir. Görünüşte temiz bir ViewSet'e sahip olmak, aslında seri hale getiricideki bir ilişkisel alanın tetiklediği sorgular nedeniyle gizli bir performans kaybına neden olabilir.

Bu nedenle, ilişkisel bir alanın seri hale getiricide kullanılıp kullanılmadığını her zaman kontrol edin ve aşağıdaki basit kuralı uygulayın:

Seri hale getiricide erişilen her ilişkisel alan için sorgunun `select_related` veya `prefetch_related` ile optimize edildiğinden emin olun.

Her PR incelemesinde bu kontrol listesine bir madde olarak ekleyin. Unutmayın: küçük bir optimizasyonla uygulamanızın yanıt sürelerini önemli ölçüde artırabilir ve kullanıcı deneyimini iyileştirebilirsiniz.

Yapay zeka özeti

Django REST Framework kullanarak geliştirdiğiniz API'de gizlenen performans katili N+1 sorgusunu nasıl tespit edip, sadece üç satırlık kod değişikliğiyle nasıl çözebileceğinizi öğrenin.

Yorumlar

00
YORUM BIRAK
ID #27653Z

0 / 1200 KARAKTER

İnsan doğrulaması

9 + 4 = ?

Editör onayı sonrası yayına girer

Moderasyon · Spam koruması aktif

Henüz onaylı yorum yok. İlk yorumu sen bırak.