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()`:
ForeignKeyveOneToOneilişkileri için kullanılır. SQL düzeyindeJOINişlemi gerçekleştirerek tüm ilişkili verileri tek bir sorguda çeker.
- `prefetch_related()`:
ManyToManyFieldve ters yönlüForeignKeyiliş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
seriesilişkisini tek birJOINsorgusuyla çeken sorgutagsiliş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.