# 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ı. - Published: 2026-06-27 - Updated: 2026-07-06 - Author: SharpSkill - Tags: django, postgresql, database optimization, full text search, python - Reading time: 9 min --- 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](https://use-the-index-luke.com/sql/where-clause/the-equals-operator/concatenated-keys) 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](https://www.postgresql.org/docs/current/indexes-index-only-scans.html) 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](https://docs.djangoproject.com/en/5.2/ref/contrib/postgres/search/) 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](https://www.postgresql.org/docs/current/indexes-types.html) 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. ## 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](/blog/django/django-orm-optimizing-queries) 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](/technologies/django/interview-questions/django-caching) ve [Django öğrenme yolunun](/technologies/django) 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. ## 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/tr/blog/django/django-postgresql-indexing-full-text-search-2026