Django ORM: queries optimaliseren voor maximale prestaties

Volledige gids voor het optimaliseren van Django ORM-queries. select_related, prefetch_related, indexen, analyse van het N+1-probleem en geavanceerde technieken voor high-performance applicaties.

Optimalisatie van Django ORM-queries voor maximale prestaties

De Django ORM biedt een elegante abstractie voor de interactie met de database, maar die eenvoud kan kritieke prestatieproblemen verbergen. Een slecht geoptimaliseerde Django-applicatie kan honderden queries genereren waar één enkele zou volstaan. Deze gids bespreekt de essentiële technieken om die problemen te herkennen en op te lossen.

De gouden regel van optimalisatie

Meten gaat voor optimaliseren. Met django-debug-toolbar in ontwikkeling worden alle gegenereerde SQL-queries zichtbaar en zijn knelpunten snel op te sporen.

Het N+1-probleem begrijpen

Het N+1-probleem is de meest voorkomende valkuil bij ORMs. Het ontstaat wanneer een eerste query N objecten ophaalt en vervolgens N extra queries worden uitgevoerd om de relaties van elk object te benaderen. Deze vermenigvuldiging van queries verslechtert de prestaties aanzienlijk.

python
# models.py
from django.db import models

class Author(models.Model):
    """Model dat de auteur van een boek voorstelt."""
    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):
    """Model dat een boek met zijn auteur voorstelt."""
    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

Deze eenvoudige modellen maken het mogelijk het N+1-probleem concreet te illustreren.

python
# views.py - Probleemvoorbeeld
def list_books_bad(request):
    """❌ Deze view genereert N+1 queries."""
    books = Book.objects.all()  # 1 query voor de boeken

    for book in books:
        # Elke toegang tot book.author triggert een SQL-query
        print(f"{book.title} door {book.author.name}")

    # Met 100 boeken = 101 SQL-queries!
    return render(request, 'books/list.html', {'books': books})

De code lijkt onschuldig maar genereert per boek een query om de bijbehorende auteur op te halen.

De methode select_related voert een SQL-JOIN uit en haalt gerelateerde gegevens op in één enkele query. Ze werkt voor ForeignKey- en OneToOneField-relaties.

python
# views.py - Geoptimaliseerde oplossing met select_related
def list_books_optimized(request):
    """✅ Deze view genereert één query met JOIN."""
    # select_related voert een SQL-JOIN uit
    books = Book.objects.select_related('author').all()

    for book in books:
        # Geen extra query: author is al geladen
        print(f"{book.title} door {book.author.name}")

    # Totaal: 1 enkele SQL-query, ongeacht het aantal boeken
    return render(request, 'books/list.html', {'books': books})

De gegenereerde SQL-query gebruikt een LEFT OUTER JOIN om auteurs samen met de boeken op te halen.

python
# select_related ketenen voor geneste relaties
# 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):
    """Haalt boeken, auteurs en uitgevers in één query op."""
    books = Book.objects.select_related(
        'author',      # ForeignKey naar Author
        'publisher'    # ForeignKey naar Publisher
    ).all()

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

Meerdere relaties kunnen tegelijk worden geoptimaliseerd door ze op te sommen in select_related.

Voor ManyToMany-relaties of omgekeerde relaties (ForeignKey vanaf de andere kant) voert prefetch_related aparte maar geoptimaliseerde queries uit en vermijdt daarmee zware joins.

python
# models.py
class Tag(models.Model):
    """Tags om boeken te categoriseren."""
    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')

De ManyToMany-relatie tussen Book en Tag vereist prefetch_related voor een efficiënte optimalisatie.

python
# views.py - ManyToMany-optimalisatie
def list_books_with_tags(request):
    """✅ Haalt boeken en hun tags efficiënt op."""
    books = Book.objects.prefetch_related('tags').all()

    for book in books:
        # Tags zijn voorgeladen, geen extra query
        tag_names = [tag.name for tag in book.tags.all()]
        print(f"{book.title}: {', '.join(tag_names)}")

    # Totaal: 2 queries (boeken + tags), ongeacht het volume
    return render(request, 'books/list.html', {'books': books})

prefetch_related voert een aparte query uit voor de tags en voert de join vervolgens in Python uit.

select_related vs prefetch_related

Gebruik select_related voor ForeignKey en OneToOne (SQL-JOIN). Gebruik prefetch_related voor ManyToMany en omgekeerde relaties (aparte queries). Beide kunnen op dezelfde QuerySet worden gecombineerd.

Voorladen aanpassen met Prefetch-objecten

Het Prefetch-object biedt fijnmazige controle over de voorgeladen data: filteren, sorteren en zelfs het aantal resultaten beperken.

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

def list_authors_with_recent_books(request):
    """Haalt auteurs op met enkel hun recente boeken."""
    # Aangepaste Prefetch: alleen boeken vanaf 2025
    recent_books_prefetch = Prefetch(
        'books',
        queryset=Book.objects.filter(
            published_date__year__gte=2025
        ).order_by('-published_date'),
        to_attr='recent_books'  # Opgeslagen in een aangepast attribuut
    )

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

    for author in authors:
        # Toegang via het aangepaste attribuut
        for book in author.recent_books:
            print(f"{author.name}: {book.title}")

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

Het attribuut to_attr slaat de resultaten op in een Python-lijst in plaats van de gebruikelijke manager.

python
# Geavanceerde combinatie: select_related + Prefetch
def list_authors_complete(request):
    """Volledig voorbeeld van optimalisatie op meerdere niveaus."""
    authors = Author.objects.prefetch_related(
        Prefetch(
            'books',
            queryset=Book.objects.select_related(
                'publisher'  # Optimaliseert ook de uitgever van elk boek
            ).prefetch_related(
                'tags'       # En de tags van elk boek
            ).filter(published_date__year=2026)
        )
    ).all()

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

Deze aanpak verlaagt het aantal queries voor complexe datastructuren drastisch.

Klaar om je Django gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Kolommen beperken met only() en defer()

Standaard haalt Django alle kolommen van de tabel op. Bij modellen met veel velden of grote velden verbetert het beperken van kolommen de prestaties.

python
# views.py
def list_books_minimal(request):
    """Haalt alleen de noodzakelijke kolommen op."""
    # only() bepaalt welke kolommen worden opgenomen
    books = Book.objects.only(
        'id',
        'title',
        'published_date'
    ).select_related('author')

    # Let op: toegang tot een niet opgenomen veld triggert een query
    for book in books:
        print(book.title)  # OK, opgenomen
        # print(book.isbn)  # Zou een extra query veroorzaken

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

De methode only() maakt een 'deferred' object aan dat alleen de opgegeven kolommen laadt.

python
# defer() om specifieke kolommen uit te sluiten
def list_authors_without_bio(request):
    """Sluit grote, zelden gebruikte velden uit."""
    # defer() sluit de opgegeven kolommen uit
    authors = Author.objects.defer(
        'bio'  # Het TextField wordt niet geladen
    ).all()

    for author in authors:
        print(author.name)   # OK
        print(author.email)  # OK
        # author.bio zou het veld op aanvraag laden

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

defer() is de tegenhanger van only(): de opgesomde kolommen worden niet initieel geladen.

Optimaliseren met values() en values_list()

Wanneer alleen bepaalde waarden nodig zijn zonder volledige modelobjecten, leveren values() en values_list() lichtere dictionaries of tuples.

python
# views.py
def get_book_titles(request):
    """Haalt alleen de titels op als een lijst."""
    # values_list levert tuples
    titles = Book.objects.values_list('title', flat=True)
    # Resultaat: ['Boek 1', 'Boek 2', ...]

    # values levert dictionaries
    book_data = Book.objects.values('title', 'published_date')
    # Resultaat: [{'title': 'Boek 1', 'published_date': ...}, ...]

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

Deze methoden vermijden het instantiëren van modelobjecten en verlagen het geheugenverbruik.

python
# Combinatie met aggregaties
from django.db.models import Count, Avg

def get_author_statistics(request):
    """Statistieken per auteur zonder objecten te laden."""
    stats = Author.objects.values('name').annotate(
        book_count=Count('books'),
        avg_year=Avg('books__published_date__year')
    ).order_by('-book_count')

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

Met annotaties worden berekeningen rechtstreeks in de database uitgevoerd.

Indexen aanmaken om queries te versnellen

Database-indexen versnellen zoekopdrachten drastisch. Django laat toe ze rechtstreeks in de modellen te definiëren.

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:
        # Samengestelde indexen voor frequente queries
        indexes = [
            # Index op publicatiedatum (frequente sortering)
            models.Index(
                fields=['published_date'],
                name='book_pub_date_idx'
            ),
            # Samengestelde index voor filteren op auteur + status
            models.Index(
                fields=['author', 'status'],
                name='book_author_status_idx'
            ),
            # Gedeeltelijke index: alleen beschikbare boeken
            models.Index(
                fields=['published_date'],
                name='book_available_idx',
                condition=models.Q(status='available')
            ),
        ]
        # Standaardsortering die de index benut
        ordering = ['-published_date']

Deze indexen verbeteren de prestaties van queries die op deze kolommen filteren.

Indexen en schrijfbewerkingen

Indexen versnellen leesbewerkingen maar vertragen schrijfbewerkingen (INSERT, UPDATE) iets. Maak ze enkel aan op kolommen die vaak in WHERE-, ORDER BY- of JOIN-clausules voorkomen.

Gebruikmaken van ruwe SQL-queries indien nodig

Voor complexe queries of databasespecifieke optimalisaties bieden ruwe SQL-queries volledige controle.

python
# views.py
from django.db import connection

def get_books_with_raw_sql(request):
    """Ruwe query voor speciale gevallen."""
    # Methode 1: raw() om modelobjecten op te halen
    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):
    """Directe uitvoering voor niet-SELECT-queries."""
    with connection.cursor() as cursor:
        # Query met complexe aggregatie
        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})

Ruwe queries omzeilen de ORM maar verliezen de databaseportabiliteit.

Queries analyseren met django-debug-toolbar

Met de tool django-debug-toolbar zijn alle SQL-queries van een Django-view zichtbaar.

python
# settings.py - Configuratie van de debug toolbar
INSTALLED_APPS = [
    # ... andere apps
    'debug_toolbar',
]

MIDDLEWARE = [
    'debug_toolbar.middleware.DebugToolbarMiddleware',
    # ... andere middlewares
]

# Toolbar tonen voor lokale aanvragen
INTERNAL_IPS = ['127.0.0.1']

DEBUG_TOOLBAR_PANELS = [
    'debug_toolbar.panels.sql.SQLPanel',      # SQL-queries
    'debug_toolbar.panels.timer.TimerPanel',  # Uitvoeringstijd
    'debug_toolbar.panels.cache.CachePanel',  # Cache
]

Deze configuratie activeert de panelen die het nuttigst zijn voor optimalisatie.

python
# Logging van SQL-queries in ontwikkeling
# settings.py
LOGGING = {
    'version': 1,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'level': 'DEBUG',
            'handlers': ['console'],
        },
    },
}

Deze logging toont elke SQL-query in de console en helpt N+1-problemen op te sporen.

QuerySets cachen

Voor data die vaak wordt opgevraagd en zelden verandert, voorkomt caching herhaalde queries.

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

def list_featured_books(request):
    """Haalt uitgelichte boeken op met cache."""
    cache_key = 'featured_books_list'

    # Poging om uit de cache te halen
    books = cache.get(cache_key)

    if books is None:
        # Cache miss: query uitvoeren
        books = list(
            Book.objects.select_related('author')
            .filter(featured=True)
            .order_by('-published_date')[:10]
        )
        # 5 minuten in de cache opslaan
        cache.set(cache_key, books, timeout=300)

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

# Decorator om de hele view te cachen
@cache_page(60 * 15)  # Cache 15 minuten
def list_all_tags(request):
    """Toont alle tags (zelden gewijzigde data)."""
    tags = Tag.objects.annotate(
        book_count=Count('books')
    ).order_by('-book_count')

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

De cache verlaagt de databasebelasting voor statische data.

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Conclusie

De optimalisatie van Django ORM-queries steunt op een paar fundamentele principes:

Meten vóór optimaliseren: gebruik django-debug-toolbar om reële problemen op te sporen

Het N+1-probleem elimineren: select_related voor ForeignKey, prefetch_related voor ManyToMany

Data beperken: only(), defer(), values() om alleen het nodige te laden

Slim indexeren: indexen aanmaken op vaak gefilterde of gesorteerde kolommen

Strategisch cachen: zelden gewijzigde data profiteert van Django's cache

Ruwe queries: als laatste redmiddel voor databasespecifieke optimalisaties

Methodisch toegepast veranderen deze technieken een trage Django-applicatie in een prestatiegerichte oplossing die grote datavolumes aankan. De Django ORM blijft een krachtig hulpmiddel zodra de subtiliteiten worden beheerst.

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Tags

#django
#django orm
#python
#database optimalisatie
#prestaties

Delen

Gerelateerde artikelen