Yazılarımız

Veri Akademi

DJANGO ORM VE QUERYSET NEDİR? SORGU YAZIMI VE PERFORMANS İPUÇLARI

Django ile çalışırken veriye erişim “SQL yazmak mı, yoksa ORM kullanmak mı?” ikileminden çok daha fazlasıdır. Asıl konu, doğru modeli kurup doğru sorguyu yazabilmek ve bunu üretimde sürdürülebilir bir performansla yapabilmektir. Bu noktada Django ORM ve özellikle QuerySet kavramı, hem okunabilirliği hem de hız optimizasyonunu aynı anda yönetmenizi sağlar.

QuerySet, veritabanına gönderilecek sorgunun “tembel” (lazy) bir temsilidir: Zincirleyebilir, parçalayabilir, yeniden kullanabilir; en önemlisi de ne zaman çalışacağını kontrol edebilirsiniz. Bu sayede uygulama tarafında gereksiz bellek kullanımı ve veritabanında gereksiz yük oluşturmadan, ihtiyacınıza uygun sonuçları üretebilirsiniz. Doğru yerde doğru yöntem seçildiğinde ORM, karmaşık SQL’leri bile anlaşılır bir biçime dönüştürür.

Bu makalede Django ORM’nin temel yaklaşımını, QuerySet’in nasıl çalıştığını, sık kullanılan sorgu kalıplarını ve performans tarafında kritik ipuçlarını gerçekçi örneklerle ele alacağız. İsterseniz konuyu daha sistematik ilerletmek için Django Eğitimi sayfasındaki kapsamı da inceleyebilirsiniz.

Django kabuğunda zincirlenen sorgularla filtrelenen kayıtların okunabilir bir akışta listelenmesi

Django ORM’ye Hızlı Bakış

Django ORM (Object-Relational Mapping), Python sınıflarıyla tanımladığınız modelleri veritabanı tablolarına bağlar. Böylece uygulama kodunda nesnelerle çalışırken, altta veritabanı işlemleri güvenli ve tutarlı biçimde yürütülür. ORM’nin cazibesi yalnızca “SQL yazmamak” değildir; asıl kazanç, tekrar kullanılabilir sorgu kalıpları ve tutarlı veri erişimidir.

ORM’nin merkezinde model katmanı vardır: alan türleri, ilişkiler, doğrulama davranışı ve migration’lar bu katmanda şekillenir. Bu tasarım sağlıklı değilse, QuerySet ile yaptığınız optimizasyonlar da sınırlı kalır. Bu nedenle performans konuşurken bile önce modelleme ve ilişki seçimlerine bakmak gerekir.

Model tanımı ve migration ilişkisi

Model alanları (ör. ForeignKey, ManyToManyField, Index) yalnızca Python tarafını değil, veritabanı şemasını da belirler. Migration’lar, şema değişikliklerini kontrollü biçimde taşır. Üretimde “sorgu yavaş” dediğinizde, çoğu zaman temel sebep eksik indeks, yanlış ilişki yönü veya gereksiz join’dir; bunlar model/migration seviyesinde çözülür.

Manager ve QuerySet farkı

Manager (genellikle objects) bir model için başlangıç noktasıdır; QuerySet ise zincirlenebilen sorgu nesnesidir. Manager metotları çoğu zaman QuerySet döndürür ve bu dönüş, sorguları bir “pipeline” gibi kurmanızı sağlar. Kendi manager/QuerySet sınıflarınızı tanımlayarak “aktif kayıtlar”, “yayındaki içerikler” gibi iş kurallarını merkezileştirmek de mümkündür.

QuerySet Temelleri: Filtreleme, Sıralama, Dilimleme

QuerySet ile yazılan sorgular genellikle zincirleme ilerler. filter(), exclude(), order_by(), values() gibi metotlarla koşulları ekler, ardından değerlendirme tetiklenene kadar sorguyu “tanım” halinde tutarsınız. Değerlendirme; iterasyon, list(), len(), exists(), first() gibi çağrılarla gerçekleşir.

Temel hedef, veriyi uygulama tarafına “ihtiyacınız kadar” almak ve gereksiz satır/kolon taşımamaktır. Bu, sadece hız değil; daha az bellek tüketimi, daha az ağ trafiği ve daha iyi ölçeklenebilirlik demektir.

filter, exclude, get ve hata senaryoları

filter() çoklu sonuç döndürebilir; get() tek sonuç bekler ve bulunamazsa DoesNotExist, birden fazlaysa MultipleObjectsReturned fırlatır. Üretimde sağlam akış için bu ayrımı bilinçli kullanmak önemlidir. “Tek kayıt olacak” varsayımı zayıfsa, filter().first() yaklaşımı daha güvenli olabilir.

Q objeleriyle dinamik koşullar

Kullanıcıya göre koşul üretmek, opsiyonel filtreler eklemek veya OR mantığı kurmak için Q objeleri idealdir. Ayrıca karmaşık mantığı parçalara ayırıp okunabilir tutmak da kolaylaşır. Bu yaklaşım özellikle arama ekranları, raporlama panelleri ve çok kriterli listelemelerde sık görülür.

from django.db.models import Q
from store.models import Product

# Basit filtreleme ve sıralama
qs = Product.objects.filter(is_active=True).order_by('-created_at')

# Dinamik arama: isimde veya açıklamada geçen kelime
term = "keyboard"
search_q = Q(name__icontains=term) | Q(description__icontains=term)

# Fiyat aralığı ve stok kontrolü ile birleşik sorgu
filtered = (
    qs.filter(search_q)
      .filter(price__gte=500, price__lte=2500)
      .exclude(stock=0)
      .only('id', 'name', 'price', 'created_at')
)

# Değerlendirme burada olur
results = list(filtered[:20])

Burada küçük ama kritik bir ayrıntı var: only() ile sadece gereken kolonları seçmek, özellikle geniş tablolar ve sık listeleme ekranlarında ciddi bir fark yaratabilir. Ancak bu yaklaşımı kullanırken, sonradan erişeceğiniz alanların “deferred” olacağını ve ek sorgu doğurabileceğini unutmamak gerekir.

İlişkiler Üzerinden Sorgu: select_related ve prefetch_related

Django ORM’nin en güçlü taraflarından biri, ilişkiler üzerinden gezinebilmesidir. Ancak performans tarafında en çok hata da burada yapılır. Bir listeleme ekranında her satır için ilişkili nesneleri tek tek çekmek, klasik N+1 problemi doğurur. Çözüm çoğu zaman doğru “eager loading” yöntemini seçmektir.

ForeignKey ve OneToOne için select_related

select_related, ilişkili tabloyu SQL join ile aynı sorguda alır. ForeignKey/OneToOne tarafında idealdir. Özellikle admin listeleri, sipariş listeleri, içerik–yazar gibi sık kullanılan ekranlarda varsayılan olarak düşünülmelidir. Bunun karşılığında daha geniş satırlar ve join maliyeti oluşur; yani “her zaman” değil, gerçekten ihtiyaç olduğunda kullanılmalıdır.

ManyToMany ve ters ilişki için prefetch_related

prefetch_related ise ayrı sorgularla ilişkili verileri alır ve Python tarafında birleştirir. ManyToMany veya ters ilişkilerde (ör. author.book_set) daha doğru seçenektir. Buradaki amaç, N+1’i “2 sorguya” indirmek ve tekrar eden sorgu maliyetini yok etmektir.

Sipariş listesindeki müşteri ve kalem bilgileri için join ve önceden yükleme stratejisinin karşılaştırılması

İlişki optimizasyonu yapılırken, sonuç kümelerinin boyutu da dikkate alınmalı. Çok büyük bir listeyi komple prefetch etmek, bellek baskısı yaratabilir. Bu nedenle sayfalama, alan seçimi ve ihtiyaç anında yükleme yaklaşımı birlikte değerlendirilmelidir.

Performans İpuçları: Alan Seçimi, Hesaplamalar, Kısa Yol Kontrolleri

QuerySet performansını artırmanın en pratik yolları, gereksiz veriyi azaltmak ve pahalı işlemleri doğru yere taşımaktır. Uygulama tarafında döngüyle hesaplama yapmak yerine veritabanında toplulaştırma (aggregate) veya satır bazlı türetme (annotate) kullanmak çoğu senaryoda daha verimlidir. Ayrıca “var mı?” gibi soruları exists() ile sormak, gereksiz veri çekmeyi engeller.

annotate ve aggregate ile raporlama

annotate ile her satıra hesaplanmış alanlar ekleyebilir, aggregate ile toplam sonuç döndürebilirsiniz. Bu, özellikle satış raporu, içerik etkileşimi, sepet toplamı gibi ekranlarda iş görür. Önemli olan, doğru indekslerle desteklemek ve gereksiz join’leri sınırlamaktır.

Sadece gereken alanları taşımak: values, values_list, only, defer

Listeleme ekranı sadece id ve name gösteriyorsa tüm modeli taşımak gereksizdir. values() ve values_list() ile doğrudan sözlük/tuple döndürmek daha hafif olabilir. Diğer yandan model örneği gerekli ise only() veya defer() ile alanları kontrol etmek işe yarar. Burada amaç, ihtiyaca göre en uygun “çıktı formatını” seçmektir.

from django.db.models import Sum, Count, F
from django.utils import timezone
from store.models import Order

start = timezone.now().replace(day=1, hour=0, minute=0, second=0, microsecond=0)

# Ay içindeki tamamlanan siparişleri al
base = Order.objects.filter(status="completed", created_at__gte=start)

# Müşteri bazında toplam harcama ve sipariş sayısı
report = (
    base.values("customer_id")
        .annotate(
            order_count=Count("id"),
            total_spent=Sum("total_amount"),
        )
        .order_by("-total_spent")
)

top_customers = list(report[:50])

# Basit bir kısa yol: sadece var mı?
has_any = base.exists()

# Bazı durumlarda F ifadeleri ile veri tabanında güncelleme
# (örn. puan biriktirme)
updated = (
    base.filter(customer__is_vip=True)
        .update(vip_points=F("vip_points") + 10)
)
  • exists() kontrolü, liste çekmeye göre daha hafif bir SQL üretir.
  • values() ile model yerine sade veri taşımak, API yanıtlarını hızlandırabilir.
  • Annotate kullanımında, sorgu planını etkileyen join’ler gözden geçirilmeli.
  • Sayfalama (pagination) ve dilimleme, büyük tablolarda ilk savunma hattıdır.

Sorguları Görmek ve Optimize Etmek: Debug, explain, İndeks

Performans iyileştirmek için önce sorgunun ne ürettiğini görmek gerekir. Django ORM, çoğu zaman tahmin edilebilir SQL üretir; fakat zincirleme metotlar, ilişkiler ve annotate/aggregate kombinasyonları karmaşık hale gelebilir. Bu yüzden “sorgu kaç kere çalıştı, hangi join’ler var, indeks kullanıyor mu?” sorularını somut verilerle cevaplamak önemlidir.

QuerySet.query, loglama ve geliştirme araçları

Geliştirme ortamında QuerySet’in SQL karşılığını qs.query üzerinden incelemek, hatalı filtreleri yakalamaya yardımcı olur. Ayrıca Django Debug Toolbar benzeri araçlarla bir sayfada çalışan sorgu sayısı, süreleri ve tekrar eden sorgular görünür hale gelir. Özellikle N+1 gibi sorunlar bu şekilde hızla tespit edilir.

explain ile sorgu planı ve indeks stratejisi

Veritabanı tarafında sorgu planı, gerçek performansı belirler. Django’nun QuerySet.explain() çıktısı (kullanılan veritabanına göre değişir) indeks kullanımı, join yöntemleri ve tarama maliyetleri hakkında fikir verir. Sık kullanılan filtre alanlarına uygun indeksler eklemek, bir anda dramatik hız artışı sağlayabilir. Bu yaklaşım, “kodu hızlandırma”dan çok “doğru erişim yolunu” tanımlamaktır.

Sorgu planında indeks taraması ve sıralama maliyetinin işaretlendiği bir analiz ekranı düzeni

İndeks seçiminde tek bir doğru yoktur; kullanım senaryosu belirleyicidir. Örneğin tarih aralığı filtreleri yoğun ise tarih alanında indeks; kullanıcı–durum kombinasyonu sık ise bileşik indeks tercih edilebilir. Bununla birlikte, aşırı indeks eklemek yazma performansını düşürebilir; dengeli bir tasarım hedeflenmelidir.

Yaygın Tuzaklar ve İyi Pratikler

QuerySet ile verimli çalışmanın bir kısmı “ne yapılacağını bilmek”, büyük kısmı ise “ne yapılmaması gerektiğini” tanımaktır. Sık yapılan hatalar; döngü içinde ek sorgu üretmek, listeyi gereksiz yere bellek içine almak, ilişki yüklemeyi yanlış seçmek ve raporlama hesaplarını uygulama tarafına taşımaktır.

N+1 problemi ve yanlış iterasyon

Bir liste döndürüp her eleman için ilişkili veriye erişmek, masum görünse de maliyeti büyütür. Bu yüzden, ekranda hangi alanların gerçekten gösterildiğini netleştirmek ve ona göre select_related/prefetch_related kararını vermek gerekir. Ayrıca QuerySet’i gereksiz yere list() ile “erken” değerlendirmek, sayfalama ve cache davranışını da olumsuz etkileyebilir.

Transaction, bulk işlemler ve tutarlılık

Toplu güncellemelerde tek tek save() çağırmak yerine update(), bulk_create() gibi yöntemler tercih edilebilir. Bu yaklaşım hem daha hızlıdır hem de veritabanına daha az round-trip üretir. Ancak sinyal (signals) gereksinimi, doğrulama akışı ve yan etkiler göz önünde bulundurulmalıdır. Kritik güncellemelerde transaction sınırlarıyla tutarlılık korunur.


Özetle, Django ORM ve QuerySet; doğru kullanıldığında hem geliştirici deneyimini hem de üretim performansını iyileştirir. Temel fikir, sorguyu “okunabilir bir akış” olarak kurmak ve sonuçları gereksiz yere şişirmeden ihtiyaç kadar veri çekmektir. İlişki yükleme stratejileri, alan seçimi, toplulaştırma ve indeks tasarımı bir araya geldiğinde sürdürülebilir bir hız yakalanır.

Yeni bir ekran eklerken veya mevcut bir listeyi iyileştirirken şu kontrol listesi işe yarar: Hangi kolonlar gerekli? Hangi ilişkiler ekranda kullanılıyor? Sorgu sayısı kaç? İndeks var mı? Sorgu planı ne söylüyor? Bu bakış açısı, QuerySet’i sadece bir API değil, performans yönetim aracı haline getirir.

 VERİ AKADEMİ