# 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. - Published: 2026-02-15 - Updated: 2026-04-28 - Author: SharpSkill - Tags: django, django orm, python, database optimalisatie, prestaties - Reading time: 14 min --- 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. ## N+1 oplossen met select_related 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`. ## ManyToMany-relaties optimaliseren met prefetch_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. ## 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. ## 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/nl/blog/django/django-orm-optimizing-queries