Django en PostgreSQL in 2026: indexering, full-text search en interviewvragen
Een praktische gids voor Django PostgreSQL-optimalisatie: B-tree-, partiële en covering-indexen, full-text search met SearchVector en GIN, plus interviewvragen voor 2026.

Django PostgreSQL-optimalisatie bepaalt het verschil tussen een applicatie die voorbij de eerste duizend gebruikers doorschaalt en een die bij elke lijstweergave vastloopt. PostgreSQL levert een indexeringsengine en een subsysteem voor full-text search die de meeste Django-projecten onaangeroerd laten, waardoor querysnelheid verloren gaat die niets kost om terug te winnen. Deze gids behandelt de indexeringspatronen, de zoekfuncties van django.contrib.postgres en de database-interviewvragen die in 2026 het onderscheid maken tussen medior en senior Django-engineers.
Elke index versnelt leesbewerkingen, maar vertraagt schrijfbewerkingen en verbruikt schijfruimte. Voer EXPLAIN (ANALYZE, BUFFERS) uit op de echte query, voeg de index toe en vergelijk vervolgens het plan. Een index die de query planner nooit kiest, is pure overhead bij elke INSERT en UPDATE.
PostgreSQL-indexering: de basis in Django
Standaard maakt PostgreSQL alleen een B-tree-index aan op primaire sleutels en kolommen met unique=True. Elke andere gefilterde of gesorteerde kolom voert een sequential scan uit totdat er een index bestaat. Django stelt indexen beschikbaar via Meta.indexes, wat de voorkeur verdient boven het oudere db_index=True omdat het samengestelde, partiële en covering-indexen op één plek ondersteunt.
De volgorde van kolommen binnen een samengestelde index is niet cosmetisch. Een B-tree kan een index voor een query alleen benutten wanneer het filter een voorafgaand prefix van de geïndexeerde kolommen gebruikt, een regel die uitgebreid wordt uitgelegd op Use The Index, Luke. Een index op (status, created_at) bedient WHERE status = ? ORDER BY created_at, maar diezelfde index kan een query die alleen op created_at filtert niet versnellen.
# 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 = [
# B-tree op één kolom voor exacte en range-lookups op status
models.Index(fields=["status"]),
# Samengestelde index: bedient WHERE status = ? ORDER BY created_at DESC
# De eerste kolom (status) moet in het filter staan om benut te worden
models.Index(fields=["status", "-created_at"]),
]De samengestelde index maakt van een dashboardquery als "laatste openstaande bestellingen" één enkele index scan in plaats van een filter-en-sorteer over de hele tabel.
Partiële en covering-indexen voor gerichte queries
Een partiële index dekt alleen rijen die aan een voorwaarde voldoen, waardoor hij klein blijft en in het geheugen past. Wanneer de meeste rijen dezelfde waarde delen en queries alleen de minderheid raken, is een partiële index dramatisch goedkoper dan een volledige. Een covering-index gaat verder: door niet-sleutelkolommen toe te voegen met include, beantwoordt PostgreSQL de query rechtstreeks vanuit de index zonder de tabel-heap aan te raken, een toegangspatroon dat een index-only scan heet.
# models.py
from django.db import models
from django.db.models import Q
class Order(models.Model):
# velden zoals hierboven gedefinieerd
class Meta:
indexes = [
# Partiële index: indexeert alleen openstaande rijen, negeert afgeronde historie
models.Index(
fields=["created_at"],
name="pending_orders_idx",
condition=Q(status="pending"),
),
# Covering-index: total wordt direct uit de index gelezen (index-only scan)
models.Index(
fields=["status"],
include=["total"],
name="status_total_covering_idx",
),
]In een bestellingentabel waar 2% van de rijen openstaand is, kan de partiële index vijftig keer kleiner zijn dan een volledige index op dezelfde kolom, en de planner houdt hem warm in de buffer cache. De include-clausule is gedocumenteerd onder PostgreSQL index-only scans en is de meest over het hoofd geziene winst in Django-applicaties die een numerieke kolom aggregeren achter een statusfilter.
Django full-text search met PostgreSQL
Grijpen naar title__icontains=query voelt handig, maar dwingt een sequential scan af en kan resultaten niet op relevantie rangschikken. Django full-text search omhult de native tsvector- en tsquery-types van PostgreSQL via SearchVector, SearchQuery en SearchRank. Een search vector normaliseert tekst naar lexemen, verwijdert stopwoorden en reduceert woorden tot hun stam, zodat een zoekopdracht naar "running" ook "run" en "ran" vindt.
# 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")
# Gewicht A rangschikt titeltreffers boven body-treffers (gewicht B)
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")
)De weight-parameter kent prioriteitscategorieën toe van A (hoogste) tot D. Een titeltreffer scoort hoger dan een body-treffer, zelfs wanneer beide de zoekterm bevatten, wat aansluit bij hoe gebruikers verwachten dat een zoekopdracht zich gedraagt. De volledige API en het vierledige gewichtsmodel worden behandeld in de Django full-text search reference.
GIN-indexen en SearchVectorField voor snel zoeken
De bovenstaande query berekent de search vector bij elk verzoek opnieuw voor elke rij, wat prima is voor een paar duizend rijen en onacceptabel voor een miljoen. Het productiepatroon slaat de vector op in een SearchVectorField en indexeert die met een GIN-index, het indextype dat PostgreSQL gebruikt voor samengestelde waarden zoals full-text-documenten en JSONB.
# 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()
# Opgeslagen, vooraf berekend zoekdocument
search_vector = SearchVectorField(null=True)
class Meta:
indexes = [
# GIN maakt van full-text-lookups logaritmische index scans
GinIndex(fields=["search_vector"]),
]Het opgeslagen veld moet synchroon blijven met title en body. Een post_save-signaal dat de vector schrijft met .update() voorkomt dat het signaal zichzelf recursief activeert, aangezien QuerySet.update() geen save-signalen uitzendt.
# 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"),
)Voor teams op Django 5.0 of nieuwer verwijdert een database-berekend GeneratedField of een PostgreSQL-trigger de extra UPDATE volledig door de kolom binnen de database bij te houden. Welk mechanisme de kolom ook gevuld houdt, de query filtert vervolgens rechtstreeks op het geïndexeerde veld, en de GIN-index maakt van wat een full table scan was een logaritmische lookup.
GIN-indexen versnellen leesbewerkingen tegen reële schrijfkosten, aangezien elke insert veel index-entries raakt. Stel op tabellen met veel schrijfverkeer de opslagparameter fastupdate in en bewaak de grootte van de pending-list, anders lopen full-text-schrijfbewerkingen vast achter indexonderhoud.
Het juiste PostgreSQL-indextype kiezen
B-tree is de juiste standaardkeuze, maar PostgreSQL biedt via django.contrib.postgres.indexes verschillende indextypes, en het verkeerde kiezen verspilt schijfruimte zonder ook maar één query te verbeteren. De keuze volgt de vorm van de data en de operator die de query gebruikt, zoals gecatalogiseerd in de PostgreSQL index types documentation.
| Indextype | Beste voor | Django-klasse |
|------------|----------|--------------|
| B-tree | Gelijkheid en range op scalaire kolommen | models.Index |
| GIN | Full-text search, JSONB, array-containment | GinIndex |
| BRIN | Enorme append-only-tabellen gesorteerd op een kolom | BrinIndex |
| Hash | Uitsluitend gelijkheidszoekopdrachten op grote kolommen | HashIndex |
BRIN verdient bijzondere aandacht voor time-series-data. In een tabel met tientallen miljoenen rijen die in tijdstempelvolgorde zijn ingevoegd, neemt een BrinIndex op created_at een paar kilobytes in beslag waar een B-tree honderden megabytes nodig zou hebben, omdat BRIN alleen de minimale en maximale waarde per blokbereik opslaat. De keerzijde is dat BRIN alleen helpt wanneer de fysieke rijvolgorde de geïndexeerde kolom volgt, wat vanzelf het geval is voor append-only-logs en event-tabellen.
# 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 = [
# Minieme index voor range scans op een append-only, chronologisch geordende tabel
BrinIndex(fields=["created_at"]),
]Het indextype afstemmen op het toegangspatroon is een terugkerend thema in senior-interviews, omdat het aantoont dat een kandidaat redeneert over storage layout in plaats van reflexmatig overal B-trees toe te voegen.
Klaar om je Django gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Django database-interviewvragen in 2026
Databasevragen domineren senior Django-interviews omdat vaardigheid met de ORM nog geen begrip inhoudt van wat de ORM daadwerkelijk uitstuurt. De onderstaande vragen weerspiegelen wat sollicitatiepanels in 2026 echt aftasten, en ze sluiten natuurlijk aan bij de tuning op queryniveau uit de Django ORM query optimization guide.
Wanneer helpt een samengestelde index op (a, b) een query niet? Wanneer de query niet op a filtert. Een B-tree is eerst geordend op zijn eerste kolom, dus een filter op alleen b kan de index niet benutten. Dit is de leftmost-prefix-regel, en kandidaten die deze uitleggen tonen aan dat ze de indexstructuur begrijpen in plaats van recepten uit het hoofd te kennen.
Wat is het verschil tussen select_related en prefetch_related? select_related voert een SQL JOIN uit en werkt voor foreign-key- en one-to-one-relaties in één enkele query. prefetch_related voert een tweede query uit en voegt de resultaten in Python samen, wat nodig is voor many-to-many- en reverse-foreign-key-relaties. De verkeerde kiezen mist ofwel de optimalisatie, ofwel veroorzaakt een onnodige tweede query.
Hoe verschilt icontains op databaseniveau van full-text search? icontains compileert naar ILIKE '%term%', wat geen standaard B-tree-index kan gebruiken en elke rij scant. Full-text search matcht tegen een vooraf berekende tsvector ondersteund door een GIN-index en rangschikt resultaten op relevantie. Dit onderscheid is een betrouwbaar signaal dat een kandidaat verder is gekomen dan speelgoeddatasets.
Waarom kan count() traag zijn op een grote tabel, en wat zijn de alternatieven? PostgreSQL houdt geen gecacht rijaantal per tabel bij, dus COUNT(*) scant elke zichtbare rij onder MVCC. Voor benaderende aantallen levert een query op pg_class.reltuples direct een schatting op, en voor paginering vermijdt keyset-paginering op een geïndexeerde kolom het tellen volledig. Gestructureerde oefening op deze patronen is beschikbaar in de Django caching interview module, waar cache-ondersteunde tellers een terugkerend thema zijn, en verspreid over het volledige Django learning track.
Wat onthult EXPLAIN ANALYZE dat EXPLAIN alleen niet doet? EXPLAIN toont de geschatte kosten van de planner; EXPLAIN ANALYZE voert de query daadwerkelijk uit en rapporteert echte tijden en rijaantallen. Een groot verschil tussen geschatte en werkelijke rijen wijst op verouderde tabelstatistieken, oplosbaar met ANALYZE, en is een veelvoorkomende oorzaak van een planner die een index negeert die overduidelijk van toepassing zou moeten zijn.
Begin met oefenen!
Test je kennis met onze gespreksimulatoren en technische tests.
Conclusie
- Definieer indexen in
Meta.indexesin plaats vandb_index=True, zodat samengestelde, partiële en covering-indexen op één controleerbare plek staan. - Orden de kolommen van een samengestelde index volgens de leftmost-prefix-regel: de eerste kolom moet in het queryfilter voorkomen om de index te laten werken.
- Gebruik partiële indexen wanneer queries slechts een minderheid van de rijen raken, zoals openstaande bestellingen in een tabel die wordt gedomineerd door afgeronde historie.
- Voeg covering-indexen toe met
includeom index-only scans mogelijk te maken en heap-toegang over te slaan bij leesintensieve aggregatiequeries. - Vervang zoeken met
icontainsdoorSearchVector,SearchQueryenSearchRankom gratis relevantierangschikking en stemming te krijgen. - Sla het zoekdocument op in een
SearchVectorFieldondersteund door eenGinIndex, synchroon gehouden door een signaal,GeneratedFieldof database-trigger, voordat full-text search op productieschaal komt. - Valideer indexbeslissingen altijd met
EXPLAIN (ANALYZE, BUFFERS); een index die de planner nooit kiest, is een schrijfbelasting zonder leesvoordeel.
Tags
Delen
Gerelateerde artikelen

Django async views en ASGI in 2026: performance en interviewvragen
Een diepgaande analyse van Django async views en ASGI in 2026: hoe ze onder de motorkap werken, welke server ingezet moet worden, de async ORM en de SynchronousOnlyOperation-valkuil, plus interviewvragen.

Django 6.0: Samengestelde Primaire Sleutels, Achtergrondtaken en Sollicitatievragen voor 2026
Technisch overzicht van Django 6.0: CompositePrimaryKey voor meervoudige sleutels, het native @task-framework voor achtergrondverwerking, template partials, CSP-middleware en veelgestelde Django-sollicitatievragen voor Python-ontwikkelaars.

Django en Celery: Asynchrone Taakverwerking en Sollicitatievragen 2026
Leer Django en Celery configureren voor asynchrone taakverwerking met codevoorbeelden, task routing, Celery Beat, productie-instellingen en sollicitatievragen voor 2026.