Django ORM: maksimum performans için sorgu optimizasyonu

Django ORM sorgularını optimize etmek için kapsamlı rehber. select_related, prefetch_related, indeksler, N+1 sorunu analizi ve yüksek performanslı uygulamalar için ileri teknikler.

Maksimum performans için Django ORM sorgu optimizasyonu

Django ORM, veritabanıyla etkileşim için zarif bir soyutlama sunar; ancak bu sadelik kritik performans sorunlarını gizleyebilir. Kötü optimize edilmiş bir Django uygulaması, tek bir sorgunun yeterli olabileceği durumlarda yüzlerce sorgu üretebilir. Bu rehber, söz konusu sorunları tespit etmek ve çözmek için temel teknikleri ele alır.

Optimizasyonun altın kuralı

Optimize etmeden önce ölçmek gerekir. Geliştirme ortamında django-debug-toolbar kullanmak, üretilen her SQL sorgusunu görmeyi ve darboğazları hızla saptamayı sağlar.

N+1 sorununu anlamak

N+1 sorunu, ORM dünyasının en yaygın tuzağını temsil eder. İlk sorgu N nesne getirdiğinde ve ardından her nesnenin ilişkilerine erişmek için N ek sorgu çalıştırıldığında ortaya çıkar. Bu sorgu çoğalması performansı ciddi biçimde düşürür.

python
# models.py
from django.db import models

class Author(models.Model):
    """Bir kitabın yazarını temsil eden model."""
    name = models.CharField(max_length=200)
    email = models.EmailField(unique=True)
    bio = models.TextField(blank=True)

    def __str__(self):
        return self.name

class Book(models.Model):
    """Yazarıyla birlikte bir kitabı temsil eden model."""
    title = models.CharField(max_length=300)
    author = models.ForeignKey(
        Author,
        on_delete=models.CASCADE,
        related_name='books'
    )
    published_date = models.DateField()
    isbn = models.CharField(max_length=13, unique=True)

    def __str__(self):
        return self.title

Bu sade modeller, N+1 sorununu somut biçimde göstermeye yardımcı olur.

python
# views.py - Sorunlu örnek
def list_books_bad(request):
    """❌ Bu görünüm N+1 sorgu üretir."""
    books = Book.objects.all()  # Kitaplar için 1 sorgu

    for book in books:
        # book.author'a her erişim bir SQL sorgusu tetikler
        print(f"{book.title} - {book.author.name}")

    # 100 kitap = 101 SQL sorgusu!
    return render(request, 'books/list.html', {'books': books})

Kod masum görünür ancak her kitap için ilgili yazarı getiren bir sorgu üretir.

select_related metodu bir SQL JOIN gerçekleştirir ve ilişkili verileri tek sorguda getirir. ForeignKey ve OneToOneField ilişkilerinde çalışır.

python
# views.py - select_related ile optimize çözüm
def list_books_optimized(request):
    """✅ Bu görünüm JOIN içeren tek bir sorgu üretir."""
    # select_related bir SQL JOIN yapar
    books = Book.objects.select_related('author').all()

    for book in books:
        # Ek sorgu yok: author zaten yüklü
        print(f"{book.title} - {book.author.name}")

    # Toplam: kitap sayısından bağımsız 1 SQL sorgusu
    return render(request, 'books/list.html', {'books': books})

Üretilen SQL sorgusu, yazarları kitaplarla birlikte almak için LEFT OUTER JOIN kullanır.

python
# İç içe ilişkiler için select_related zincirleme kullanımı
# models.py
class Publisher(models.Model):
    name = models.CharField(max_length=200)
    country = models.CharField(max_length=100)

class Book(models.Model):
    title = models.CharField(max_length=300)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)

# views.py
def list_books_with_details(request):
    """Kitap, yazar ve yayıncıyı tek sorguda getirir."""
    books = Book.objects.select_related(
        'author',      # Author'a ForeignKey
        'publisher'    # Publisher'a ForeignKey
    ).all()

    return render(request, 'books/list.html', {'books': books})

Birden fazla ilişki, select_related içinde sıralanarak aynı anda optimize edilebilir.

ManyToMany ilişkileri veya tersine ilişkiler (karşı taraftan ForeignKey) için prefetch_related, devasa join'lerden kaçınarak ayrı ama optimize sorgular çalıştırır.

python
# models.py
class Tag(models.Model):
    """Kitapları kategorize etmek için etiketler."""
    name = models.CharField(max_length=50, unique=True)

    def __str__(self):
        return self.name

class Book(models.Model):
    title = models.CharField(max_length=300)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    tags = models.ManyToManyField(Tag, related_name='books')

Book ve Tag arasındaki ManyToMany ilişkisi, etkili optimizasyon için prefetch_related gerektirir.

python
# views.py - ManyToMany optimizasyonu
def list_books_with_tags(request):
    """✅ Kitapları ve etiketlerini verimli biçimde getirir."""
    books = Book.objects.prefetch_related('tags').all()

    for book in books:
        # Etiketler önceden yüklendi, ek sorgu yok
        tag_names = [tag.name for tag in book.tags.all()]
        print(f"{book.title}: {', '.join(tag_names)}")

    # Toplam: 2 sorgu (kitaplar + etiketler), miktarlar fark etmeksizin
    return render(request, 'books/list.html', {'books': books})

prefetch_related etiketler için ayrı bir sorgu çalıştırır, ardından birleştirmeyi Python tarafında yapar.

select_related vs prefetch_related

ForeignKey ve OneToOne için select_related kullanılmalıdır (SQL JOIN). ManyToMany ve tersine ilişkiler için prefetch_related tercih edilir (ayrı sorgular). İkisi aynı QuerySet üzerinde birleştirilebilir.

Prefetch nesneleriyle ön yüklemeyi özelleştirmek

Prefetch nesnesi, önceden yüklenen veriler üzerinde ince denetim sağlar: filtreleme, sıralama ve hatta sonuç sayısını sınırlandırma.

python
# views.py
from django.db.models import Prefetch

def list_authors_with_recent_books(request):
    """Yazarları yalnızca güncel kitaplarıyla getirir."""
    # Özel Prefetch: yalnızca 2025'ten itibaren çıkan kitaplar
    recent_books_prefetch = Prefetch(
        'books',
        queryset=Book.objects.filter(
            published_date__year__gte=2025
        ).order_by('-published_date'),
        to_attr='recent_books'  # Özel bir nitelikte saklanır
    )

    authors = Author.objects.prefetch_related(
        recent_books_prefetch
    ).all()

    for author in authors:
        # Özel nitelik üzerinden erişim
        for book in author.recent_books:
            print(f"{author.name}: {book.title}")

    return render(request, 'authors/list.html', {'authors': authors})

to_attr niteliği sonuçları her zamanki manager yerine bir Python listesinde tutar.

python
# Gelişmiş kombinasyon: select_related + Prefetch
def list_authors_complete(request):
    """Çok seviyeli optimizasyona dair kapsamlı örnek."""
    authors = Author.objects.prefetch_related(
        Prefetch(
            'books',
            queryset=Book.objects.select_related(
                'publisher'  # Her kitabın yayıncısını da optimize eder
            ).prefetch_related(
                'tags'       # Ve her kitabın etiketlerini
            ).filter(published_date__year=2026)
        )
    ).all()

    return render(request, 'authors/complete.html', {'authors': authors})

Bu yaklaşım, karmaşık veri yapılarında sorgu sayısını çarpıcı biçimde düşürür.

Django mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

only() ve defer() ile sütun sınırlamak

Django varsayılan olarak tablonun tüm sütunlarını getirir. Çok alanlı veya büyük alanlı modellerde sütunları sınırlamak performansı iyileştirir.

python
# views.py
def list_books_minimal(request):
    """Yalnızca gerekli sütunları getirir."""
    # only() dahil edilecek sütunları belirler
    books = Book.objects.only(
        'id',
        'title',
        'published_date'
    ).select_related('author')

    # Dikkat: dahil edilmeyen bir alana erişim ek sorgu tetikler
    for book in books:
        print(book.title)  # OK, dahil
        # print(book.isbn)  # Ek bir sorguya yol açardı

    return render(request, 'books/list.html', {'books': books})

only() metodu yalnızca belirtilen sütunları yükleyen "deferred" bir nesne oluşturur.

python
# defer() ile belirli sütunları dışlamak
def list_authors_without_bio(request):
    """Nadiren kullanılan büyük alanları dışlar."""
    # defer() belirtilen sütunları dışlar
    authors = Author.objects.defer(
        'bio'  # TextField yüklenmez
    ).all()

    for author in authors:
        print(author.name)   # OK
        print(author.email)  # OK
        # author.bio talep üzerine yüklenir

    return render(request, 'authors/list.html', {'authors': authors})

defer() yöntemi only()'in tersi gibi çalışır: listelenen sütunlar başlangıçta yüklenmez.

values() ve values_list() ile optimizasyon

Tam model nesneleri yerine yalnızca belirli değerler gerektiğinde, values() ve values_list() daha hafif sözlükler veya tuple'lar döndürür.

python
# views.py
def get_book_titles(request):
    """Yalnızca başlıkları liste olarak getirir."""
    # values_list tuple döndürür
    titles = Book.objects.values_list('title', flat=True)
    # Sonuç: ['Kitap 1', 'Kitap 2', ...]

    # values sözlük döndürür
    book_data = Book.objects.values('title', 'published_date')
    # Sonuç: [{'title': 'Kitap 1', 'published_date': ...}, ...]

    return render(request, 'books/titles.html', {'titles': titles})

Bu yöntemler model nesnelerinin oluşturulmasını engelleyerek bellek tüketimini azaltır.

python
# Toplulaştırmalarla kombinasyon
from django.db.models import Count, Avg

def get_author_statistics(request):
    """Nesneleri yüklemeden yazara göre istatistikler."""
    stats = Author.objects.values('name').annotate(
        book_count=Count('books'),
        avg_year=Avg('books__published_date__year')
    ).order_by('-book_count')

    # Sonuç: [{'name': 'Yazar', 'book_count': 5, 'avg_year': 2024}, ...]
    return render(request, 'authors/stats.html', {'stats': stats})

Annotation ile hesaplamalar doğrudan veritabanında yapılabilir.

Sorguları hızlandırmak için indeks oluşturmak

Veritabanı indeksleri aramaları çarpıcı biçimde hızlandırır. Django bunları doğrudan modellerde tanımlamaya olanak tanır.

python
# models.py
from django.db import models

class Book(models.Model):
    title = models.CharField(max_length=300, db_index=True)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    published_date = models.DateField()
    isbn = models.CharField(max_length=13, unique=True)
    status = models.CharField(max_length=20, default='available')

    class Meta:
        # Sık sorgular için bileşik indeksler
        indexes = [
            # Yayın tarihi indeksi (sık sıralama)
            models.Index(
                fields=['published_date'],
                name='book_pub_date_idx'
            ),
            # Yazar + durum filtresi için bileşik indeks
            models.Index(
                fields=['author', 'status'],
                name='book_author_status_idx'
            ),
            # Kısmi indeks: yalnızca müsait kitaplar
            models.Index(
                fields=['published_date'],
                name='book_available_idx',
                condition=models.Q(status='available')
            ),
        ]
        # İndeksten faydalanan varsayılan sıralama
        ordering = ['-published_date']

Bu indeksler, ilgili sütunlarda filtreleme yapan sorguların performansını iyileştirir.

İndeksler ve yazma işlemleri

İndeksler okumayı hızlandırır, ancak yazma işlemlerini (INSERT, UPDATE) hafifçe yavaşlatır. Yalnızca WHERE, ORDER BY veya JOIN içinde sıkça kullanılan sütunlarda indeks oluşturmak gerekir.

Gerektiğinde ham SQL sorgularına başvurmak

Karmaşık sorgular veya veritabanına özgü optimizasyonlar için ham SQL sorguları tam denetim sağlar.

python
# views.py
from django.db import connection

def get_books_with_raw_sql(request):
    """Özel durumlar için ham sorgu."""
    # 1. yöntem: model nesnesi döndürmek için raw()
    books = Book.objects.raw('''
        SELECT b.*, a.name as author_name
        FROM library_book b
        INNER JOIN library_author a ON b.author_id = a.id
        WHERE b.published_date > %s
        ORDER BY b.published_date DESC
    ''', ['2025-01-01'])

    return render(request, 'books/list.html', {'books': books})

def execute_custom_query(request):
    """SELECT olmayan sorgular için doğrudan yürütme."""
    with connection.cursor() as cursor:
        # Karmaşık toplulaştırmalı sorgu
        cursor.execute('''
            SELECT
                a.name,
                COUNT(b.id) as book_count,
                AVG(EXTRACT(YEAR FROM b.published_date)) as avg_year
            FROM library_author a
            LEFT JOIN library_book b ON b.author_id = a.id
            GROUP BY a.id, a.name
            HAVING COUNT(b.id) > 2
            ORDER BY book_count DESC
        ''')
        results = cursor.fetchall()

    return render(request, 'stats.html', {'results': results})

Ham sorgular ORM'i devre dışı bırakır ancak veritabanı bağımsızlığını kaybettirir.

django-debug-toolbar ile sorgu analizi

django-debug-toolbar aracı, bir Django görünümünün ürettiği tüm SQL sorgularını görselleştirir.

python
# settings.py - Debug toolbar yapılandırması
INSTALLED_APPS = [
    # ... diğer uygulamalar
    'debug_toolbar',
]

MIDDLEWARE = [
    'debug_toolbar.middleware.DebugToolbarMiddleware',
    # ... diğer middleware'ler
]

# Yerel istekler için toolbar göster
INTERNAL_IPS = ['127.0.0.1']

DEBUG_TOOLBAR_PANELS = [
    'debug_toolbar.panels.sql.SQLPanel',      # SQL sorguları
    'debug_toolbar.panels.timer.TimerPanel',  # Yürütme süresi
    'debug_toolbar.panels.cache.CachePanel',  # Cache
]

Bu yapılandırma optimizasyon için en yararlı panelleri etkinleştirir.

python
# Geliştirme ortamında SQL sorgu loglama
# settings.py
LOGGING = {
    'version': 1,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'level': 'DEBUG',
            'handlers': ['console'],
        },
    },
}

Bu loglama her SQL sorgusunu konsolda gösterir; N+1 sorunlarını tespit etmek için faydalıdır.

QuerySet'leri önbelleğe almak

Sık erişilen ve nadiren değişen veriler için önbellek, tekrarlanan sorguları önler.

python
# views.py
from django.core.cache import cache
from django.views.decorators.cache import cache_page

def list_featured_books(request):
    """Öne çıkan kitapları önbellekle getirir."""
    cache_key = 'featured_books_list'

    # Önbellekten okuma denemesi
    books = cache.get(cache_key)

    if books is None:
        # Cache miss: sorguyu çalıştır
        books = list(
            Book.objects.select_related('author')
            .filter(featured=True)
            .order_by('-published_date')[:10]
        )
        # 5 dakika önbelleğe al
        cache.set(cache_key, books, timeout=300)

    return render(request, 'books/featured.html', {'books': books})

# Tüm görünümü önbelleğe alan dekoratör
@cache_page(60 * 15)  # 15 dakika cache
def list_all_tags(request):
    """Tüm etiketleri listeler (nadiren değişen veri)."""
    tags = Tag.objects.annotate(
        book_count=Count('books')
    ).order_by('-book_count')

    return render(request, 'tags/list.html', {'tags': tags})

Önbellek, statik veriler için veritabanı yükünü azaltır.

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Sonuç

Django ORM sorgu optimizasyonu birkaç temel ilkeye dayanır:

Optimize etmeden önce ölçmek: gerçek sorunları tespit etmek için django-debug-toolbar kullanmak

N+1 sorununu ortadan kaldırmak: ForeignKey için select_related, ManyToMany için prefetch_related

Veriyi sınırlamak: yalnızca gerekli olanı yüklemek için only(), defer(), values()

Akıllı indekslemek: sık filtrelenen veya sıralanan sütunlarda indeks oluşturmak

Stratejik önbellek: nadiren değişen veriler Django önbelleğinden faydalanır

Ham sorgular: yalnızca veritabanına özgü optimizasyonlar için son çare

Metodik biçimde uygulandığında bu teknikler, yavaş bir Django uygulamasını büyük veri hacimlerini idare edebilen verimli bir sisteme dönüştürür. Django ORM, incelikleri kavrandığında güçlü bir araç olmayı sürdürür.

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Etiketler

#django
#django orm
#python
#veritabanı optimizasyonu
#performans

Paylaş

İlgili makaleler