Django ve PostgreSQL 2026: İndeksleme, Tam Metin Arama ve Mülakat Soruları

Django PostgreSQL optimizasyonu için pratik bir rehber: B-tree, kısmi ve kapsayan indeksler, SearchVector ve GIN ile tam metin arama, ayrıca 2026 mülakat soruları.

İndeksleme ve tam metin arama ile Django PostgreSQL optimizasyonu

Django PostgreSQL optimizasyonu, ilk bin kullanıcısını aşarak ölçeklenen bir uygulama ile her liste görünümünde tıkanıp kalan bir uygulama arasındaki farktır. PostgreSQL, çoğu Django projesinin hiç dokunmadığı bir indeksleme motoru ve tam metin arama alt sistemiyle gelir; bu da hiçbir maliyeti olmadan geri kazanılabilecek sorgu performansının boşa harcanması demektir. Bu rehber, indeksleme kalıplarını, django.contrib.postgres arama araçlarını ve 2026'da orta seviye Django mühendislerini kıdemlilerden ayıran veritabanı mülakat sorularını ele alır.

İndekslemeden Önce Ölçün

İndeksler okumaları hızlandırır, ancak yazma işlemlerini yavaşlatır ve disk tüketir. Gerçek sorgu üzerinde EXPLAIN (ANALYZE, BUFFERS) çalıştırmak, indeksi eklemek ve ardından planı karşılaştırmak doğru yaklaşımdır. Sorgu planlayıcısının asla seçmediği bir indeks, her INSERT ve UPDATE işleminde saf bir ek yüktür.

Django'da PostgreSQL İndeksleme Temelleri

PostgreSQL varsayılan olarak yalnızca birincil anahtarlar ve unique=True olan sütunlar üzerinde bir B-tree indeksi oluşturur. Filtrelenen veya sıralanan diğer her sütun, bir indeks var olana kadar sıralı tarama (sequential scan) ile çalışır. Django, indeksleri Meta.indexes üzerinden sunar; bu yaklaşım eski db_index=True seçeneğine tercih edilir, çünkü bileşik (composite), kısmi (partial) ve kapsayan (covering) indeksleri tek bir yerde destekler.

Bir bileşik indeks içindeki sütun sırası kozmetik bir ayrıntı değildir. Bir B-tree, ancak filtre indekslenmiş sütunların baştan gelen bir önekini (leading prefix) kullandığında sorgu için indeksi kullanabilir; bu kural Use The Index, Luke sitesinde derinlemesine açıklanır. (status, created_at) üzerindeki bir indeks WHERE status = ? ORDER BY created_at sorgusuna hizmet eder, ancak aynı indeks yalnızca created_at üzerinde filtreleyen bir sorguyu hızlandıramaz.

python
# models.py
from django.db import models

class Order(models.Model):
    customer_email = models.EmailField()
    status = models.CharField(max_length=20)
    total = models.DecimalField(max_digits=10, decimal_places=2)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        indexes = [
            # status üzerinde tam ve aralık aramaları için tek sütunlu B-tree
            models.Index(fields=["status"]),
            # Bileşik indeks: WHERE status = ? ORDER BY created_at DESC sorgusuna hizmet eder
            # Kullanılabilmesi için baştaki sütun (status) filtrede yer almalıdır
            models.Index(fields=["status", "-created_at"]),
        ]

Bileşik indeks, "en son bekleyen siparişler" gibi bir gösterge paneli sorgusunu, tüm tablo üzerinde filtrele-sonra-sırala işlemi yerine tek bir indeks taramasına dönüştürür.

Hedefli Sorgular İçin Kısmi ve Kapsayan İndeksler

Kısmi bir indeks yalnızca bir koşulla eşleşen satırları kapsar, bu yüzden küçük kalır ve bellekte durur. Satırların çoğu aynı değeri paylaştığında ve sorgular yalnızca azınlığa dokunduğunda, kısmi bir indeks tam bir indeksten çok daha ucuzdur. Kapsayan bir indeks daha da ileri gider: include ile anahtar olmayan sütunlar eklendiğinde PostgreSQL sorguyu tablo yığınına (heap) hiç dokunmadan doğrudan indeksten yanıtlar; index-only scan adı verilen bir erişim kalıbı.

python
# models.py
from django.db import models
from django.db.models import Q

class Order(models.Model):
    # yukarıda tanımlanan alanlar

    class Meta:
        indexes = [
            # Kısmi indeks: yalnızca bekleyen satırları indeksler, tamamlanmış geçmişi yok sayar
            models.Index(
                fields=["created_at"],
                name="pending_orders_idx",
                condition=Q(status="pending"),
            ),
            # Kapsayan indeks: total doğrudan indeksten okunur (index-only scan)
            models.Index(
                fields=["status"],
                include=["total"],
                name="status_total_covering_idx",
            ),
        ]

Satırların %2'sinin bekleyen olduğu bir siparişler tablosunda, kısmi indeks aynı sütun üzerindeki tam bir indeksten elli kat daha küçük olabilir ve planlayıcı onu tampon önbellekte (buffer cache) sıcak tutar. include yan tümcesi PostgreSQL index-only scans altında belgelenmiştir ve bir durum filtresinin arkasında sayısal bir sütunu toplayan Django uygulamalarındaki en çok gözden kaçan tek kazançtır.

PostgreSQL ile Django Tam Metin Arama

title__icontains=query kullanmak pratik gibi görünür, ancak sıralı bir taramaya zorlar ve sonuçları alaka düzeyine göre sıralayamaz. Django tam metin arama, PostgreSQL'in yerel tsvector ve tsquery türlerini SearchVector, SearchQuery ve SearchRank aracılığıyla sarmalar. Bir arama vektörü metni sözcükbirimlerine (lexeme) normalleştirir, durak sözcükleri (stop words) ayıklar ve sözcükleri köklerine indirger; böylece "running" araması "run" ve "ran" ile eşleşir.

python
# views.py
from django.contrib.postgres.search import (
    SearchVector, SearchQuery, SearchRank,
)
from .models import Article

def search_articles(query_text):
    query = SearchQuery(query_text, config="english")
    # A ağırlığı başlık eşleşmelerini gövde eşleşmelerinin (B ağırlığı) üzerine sıralar
    vector = (
        SearchVector("title", weight="A")
        + SearchVector("body", weight="B")
    )
    return (
        Article.objects.annotate(rank=SearchRank(vector, query))
        .filter(rank__gte=0.1)
        .order_by("-rank")
    )

weight parametresi A (en yüksek) ile D arasında öncelik grupları atar. Bir başlık eşleşmesi, her ikisi de arama terimini içerse bile bir gövde eşleşmesinin üzerinde sıralanır; bu da kullanıcıların bir aramanın nasıl davranmasını beklediğiyle örtüşür. Tam API ve dört katmanlı ağırlıklandırma modeli Django tam metin arama referansında ele alınır.

Hızlı Arama İçin GIN İndeksleri ve SearchVectorField

Yukarıdaki sorgu, her istekte her satır için arama vektörünü yeniden hesaplar; bu birkaç bin satır için sorun değildir ama bir milyon satır için kabul edilemez. Üretim kalıbı, vektörü bir SearchVectorField içinde saklar ve onu bir GIN indeksiyle indeksler; GIN, PostgreSQL'in tam metin belgeleri ve JSONB gibi bileşik değerler için kullandığı indeks türüdür.

python
# models.py
from django.contrib.postgres.search import SearchVectorField
from django.contrib.postgres.indexes import GinIndex
from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=255)
    body = models.TextField()
    # Saklanan, önceden hesaplanmış arama belgesi
    search_vector = SearchVectorField(null=True)

    class Meta:
        indexes = [
            # GIN, tam metin aramalarını logaritmik indeks taramalarına dönüştürür
            GinIndex(fields=["search_vector"]),
        ]

Saklanan alanın title ve body ile senkron kalması gerekir. Vektörü .update() ile yazan bir post_save sinyali, sinyali özyinelemeli olarak tetiklemekten kaçınır, çünkü QuerySet.update() kaydetme (save) sinyalleri yaymaz.

python
# signals.py
from django.contrib.postgres.search import SearchVector
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Article

@receiver(post_save, sender=Article)
def sync_search_vector(sender, instance, **kwargs):
    Article.objects.filter(pk=instance.pk).update(
        search_vector=SearchVector("title", weight="A")
        + SearchVector("body", weight="B"),
    )

Django 5.0 veya daha yeni sürümü kullanan ekipler için, veritabanında hesaplanan bir GeneratedField ya da bir PostgreSQL tetikleyicisi (trigger), sütunu veritabanının içinde tutarak ek UPDATE işlemini tamamen ortadan kaldırır. Onu dolduran mekanizma ne olursa olsun, sorgu ardından indekslenmiş alanı doğrudan filtreler ve GIN indeksi eskiden tam bir tablo taraması olan işlemi logaritmik bir aramaya dönüştürür.

GIN İndekslerinin Yazma Maliyeti Yüksektir

GIN indeksleri okumaları hızlandırır ama gerçek bir yazma maliyetiyle gelir, çünkü her ekleme birçok indeks girdisine dokunur. Yazma yoğun tablolarda fastupdate depolama parametresini ayarlamak ve bekleyen liste (pending-list) boyutunu izlemek gerekir; aksi halde tam metin yazma işlemleri indeks bakımının arkasında bekler.

Doğru PostgreSQL İndeks Türünü Seçmek

B-tree doğru varsayılandır, ancak PostgreSQL django.contrib.postgres.indexes üzerinden birkaç indeks türü sunar ve yanlışını seçmek tek bir sorguyu bile iyileştirmeden diski boşa harcar. Seçim, verinin şekline ve sorgunun kullandığı operatöre bağlıdır; bu PostgreSQL indeks türleri belgelerinde kataloglanmıştır.

| İndeks türü | En uygun kullanım | Django sınıfı | |------------|----------|--------------| | B-tree | Skaler sütunlarda eşitlik ve aralık | models.Index | | GIN | Tam metin arama, JSONB, dizi içerme | GinIndex | | BRIN | Bir sütuna göre sıralı, yalnızca ekleme yapılan devasa tablolar | BrinIndex | | Hash | Büyük sütunlarda yalnızca eşitlik aramaları | HashIndex |

BRIN, zaman serisi verileri için özel bir dikkati hak eder. Zaman damgası sırasına göre eklenen onlarca milyon satırlık bir tabloda, created_at üzerindeki bir BrinIndex, bir B-tree'nin yüzlerce megabayta ihtiyaç duyacağı yerde birkaç kilobayt kaplar, çünkü BRIN her blok aralığı için yalnızca minimum ve maksimum değeri saklar. Ödünleşim, BRIN'in yalnızca fiziksel satır sırası indekslenen sütunu izlediğinde yardımcı olmasıdır; bu da yalnızca ekleme yapılan günlükler ve olay tablolarında doğal olarak geçerlidir.

python
# models.py
from django.contrib.postgres.indexes import BrinIndex
from django.db import models

class Event(models.Model):
    payload = models.JSONField()
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        indexes = [
            # Yalnızca ekleme yapılan, zaman sıralı tabloda aralık taramaları için minik indeks
            BrinIndex(fields=["created_at"]),
        ]

İndeks türünü erişim kalıbıyla eşleştirmek, kıdemli mülakatlarda tekrar eden bir temadır, çünkü bir adayın her yere refleksle B-tree eklemek yerine depolama düzeni üzerine düşündüğünü kanıtlar.

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

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

2026'da Django Veritabanı Mülakat Soruları

Veritabanı soruları kıdemli Django mülakatlarına hakimdir, çünkü ORM akıcılığı, ORM'in ürettiği sorguları anlamak anlamına gelmez. Aşağıdaki sorular, 2026'da işe alım panellerinin gerçekten irdelediği konuları yansıtır ve Django ORM sorgu optimizasyonu rehberinde ele alınan sorgu düzeyindeki ayarlamalarla doğal olarak eşleşir.

(a, b) üzerindeki bir bileşik indeks bir sorguya ne zaman yardımcı olmaz? Sorgu a üzerinde filtrelemediğinde. Bir B-tree önce baştaki sütununa göre sıralıdır, bu yüzden yalnızca b üzerindeki bir filtre indeksi kullanamaz. Bu, en soldaki önek (leftmost-prefix) kuralıdır ve bunu açıklayan adaylar, ezberlenmiş tarifler yerine indeks yapısını anladıklarını gösterir.

select_related ile prefetch_related arasındaki fark nedir? select_related bir SQL JOIN gerçekleştirir ve tek bir sorguda yabancı anahtar (foreign-key) ile bire bir ilişkiler için çalışır. prefetch_related ikinci bir sorgu çalıştırır ve sonuçları Python içinde birleştirir; bu, çoka çok ve ters yabancı anahtar ilişkileri için gereklidir. Yanlışını seçmek ya optimizasyonu kaçırır ya da gereksiz bir ikinci sorgu tetikler.

icontains, veritabanı düzeyinde tam metin aramadan nasıl farklıdır? icontains, ILIKE '%term%' ifadesine derlenir; bu standart bir B-tree indeksini kullanamaz ve her satırı tarar. Tam metin arama, bir GIN indeksiyle desteklenen önceden hesaplanmış bir tsvector ile eşleşir ve sonuçları alaka düzeyine göre sıralar. Bu ayrım, bir adayın oyuncak veri kümelerinin ötesine geçtiğine dair güvenilir bir işarettir.

count() neden büyük bir tabloda yavaş olabilir ve alternatifleri nelerdir? PostgreSQL bir tablo için önbelleğe alınmış bir satır sayısı tutmaz, bu yüzden COUNT(*) MVCC altında görünür her satırı tarar. Yaklaşık sayımlar için pg_class.reltuples sorgulamak anında bir tahmin döndürür ve sayfalama için indekslenmiş bir sütunda anahtar tabanlı (keyset) sayfalama, saymaktan tamamen kaçınır. Bu kalıplar üzerinde yapılandırılmış pratik, önbellek destekli sayaçların tekrar eden bir tema olduğu Django önbellekleme mülakat modülünde ve Django öğrenme yolunun tamamında bulunur.

EXPLAIN ANALYZE, tek başına EXPLAIN'in göstermediği neyi ortaya çıkarır? EXPLAIN, planlayıcının tahmini maliyetini yazdırır; EXPLAIN ANALYZE sorguyu gerçekten çalıştırır ve gerçek süreler ile satır sayılarını raporlar. Tahmini ve gerçek satırlar arasındaki büyük bir fark, ANALYZE ile düzeltilebilen eski tablo istatistiklerine işaret eder ve planlayıcının açıkça uygulanması gereken bir indeksi göz ardı etmesinin yaygın bir kök nedenidir.

Pratik yapmaya başla!

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

Sonuç

  • Bileşik, kısmi ve kapsayan indekslerin tek bir denetlenebilir yerde toplanması için indeksler db_index=True yerine Meta.indexes içinde tanımlanmalıdır.
  • Bileşik indeks sütunları en soldaki önek kuralına göre sıralanmalıdır: indeksin uygulanabilmesi için baştaki sütun sorgu filtresinde yer almalıdır.
  • Sorguların yalnızca satırların azınlığına dokunduğu durumlarda kısmi indeksler kullanılmalıdır; örneğin tamamlanmış geçmişin baskın olduğu bir tablodaki bekleyen siparişler.
  • Okuma yoğun toplama sorgularında index-only scan sağlamak ve yığın erişimini atlamak için include ile kapsayan indeksler eklenmelidir.
  • Alaka düzeyi sıralaması ve kök bulmayı (stemming) bedava elde etmek için icontains araması SearchVector, SearchQuery ve SearchRank ile değiştirilmelidir.
  • Tam metin arama üretim ölçeğine ulaşmadan önce arama belgesi; bir sinyal, GeneratedField veya veritabanı tetikleyicisiyle senkron tutulan bir GinIndex destekli bir SearchVectorField içinde saklanmalıdır.
  • İndeks kararları her zaman EXPLAIN (ANALYZE, BUFFERS) ile doğrulanmalıdır; planlayıcının asla seçmediği bir indeks, okuma zamanı faydası olmayan bir yazma zamanı vergisidir.

Etiketler

#django
#postgresql
#database optimization
#full text search
#python

Paylaş

İlgili makaleler