Performance Optimization
Performance is critical for GraphQL APIs, especially when dealing with complex queries and large datasets. This guide covers strategies to optimize your Strawberry Django application for maximum performance.
Table of Contents
- Overview
- The N+1 Query Problem
- Query Optimizer
- DataLoaders
- Database Optimization
- Caching Strategies
- Query Complexity
- Pagination
- Monitoring and Profiling
- Best Practices
- Common Patterns
- Troubleshooting
Overview
GraphQLβs flexibility can lead to performance issues if not handled properly. Key challenges:
- N+1 queries - Multiple database queries for related objects
- Over-fetching - Retrieving more data than needed
- Complex queries - Deeply nested or expensive operations
- Duplicate queries - Same data fetched multiple times
Strawberry Django provides several tools to address these:
- Query Optimizer - Automatic
select_related()andprefetch_related() - DataLoaders - Batching and caching for custom data fetching
- Pagination - Limit result sets to manageable sizes
- Caching - Store computed results
The N+1 Query Problem
The N+1 problem occurs when fetching a list of objects (1 query) and then fetching related objects for each item (N queries).
Example Problem
class Author(models.Model): name = models.CharField(max_length=100)
class Book(models.Model): title = models.CharField(max_length=200) author = models.ForeignKey(Author, on_delete=models.CASCADE)
# schema.pyimport strawberryimport strawberry_django
@strawberry_django.type(Author)class AuthorType: name: strawberry.auto
@strawberry_django.type(Book)class BookType: title: strawberry.auto author: AuthorType # N+1 problem here!
@strawberry.typeclass Query: @strawberry.field def books(self) -> list[BookType]: return Book.objects.all() query { books { # 1 query title author { # N queries (one per book!) name } }} Without optimization: 1 + N queries (if 100 books = 101 queries!)
Solution: Query Optimizer Extension
import strawberryfrom strawberry_django.optimizer import DjangoOptimizerExtension
schema = strawberry.Schema( query=Query, extensions=[ DjangoOptimizerExtension(), # Automatically optimizes queries ]) With optimizer: 2 queries (1 for books + 1 JOIN for authors)
The optimizer automatically:
- Uses
select_related()for foreign keys and one-to-one relationships - Uses
prefetch_related()for many-to-many and reverse foreign keys - Adds
only()to fetch only requested fields (turned off for mutations) - Handles nested relationships
Query Optimizer
The query optimizer analyzes your GraphQL query and optimizes the database queries.
Basic Usage
import strawberryfrom strawberry_django.optimizer import DjangoOptimizerExtension
schema = strawberry.Schema( query=Query, mutation=Mutation, extensions=[ DjangoOptimizerExtension(), ]) How It Works
class Publisher(models.Model): name = models.CharField(max_length=100)
class Author(models.Model): name = models.CharField(max_length=100) publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
class Book(models.Model): title = models.CharField(max_length=200) author = models.ForeignKey(Author, on_delete=models.CASCADE) isbn = models.CharField(max_length=13) query { books { title isbn author { name publisher { name } } }} Without optimizer:
# Query 1: Get all booksBook.objects.all()
# Query 2-N: Get author for each bookAuthor.objects.get(id=book.author_id)
# Query N+1-2N: Get publisher for each authorPublisher.objects.get(id=author.publisher_id) With optimizer:
# Single optimized queryBook.objects.all() \ .select_related('author__publisher') \ .only('title', 'isbn', 'author__name', 'author__publisher__name') Manual Optimization Hints
You can provide hints to the optimizer using field options:
import strawberryfrom strawberry_django import field
@strawberry_django.type(Book)class BookType: title: str author: AuthorType = field( # Optimization hints select_related=['author__publisher'], prefetch_related=['author__books'], only=['author__name'], ) Disabling Optimizer for Specific Fields
from strawberry_django import field
@strawberry_django.type(Book)class BookType: title: str
# Disable optimizer for custom logic @field(disable_optimization=True) def computed_field(self) -> str: # Custom logic that doesn't benefit from optimization return self.do_custom_calculation() Annotate for Aggregations
from django.db.models import Count, Avgfrom strawberry_django import field
@strawberry_django.type(Author)class AuthorType: name: str
# Annotate with aggregation book_count: int = field( annotate={'book_count': Count('books')} )
avg_rating: float = field( annotate={'avg_rating': Avg('books__rating')} ) DataLoaders
For complex scenarios where the optimizer isnβt enough, use DataLoaders.
When to Use DataLoaders
Use DataLoaders when:
- Fetching data from external APIs
- Complex computed values requiring multiple queries
- Custom aggregations or calculations
- Non-standard relationship patterns
See the DataLoaders Guide for comprehensive documentation.
Basic DataLoader Pattern
from strawberry.dataloader import DataLoaderfrom typing import List
async def load_authors(keys: List[int]) -> List[Author]: """Batch load authors by ID""" authors = Author.objects.filter(id__in=keys) author_map = {author.id: author for author in authors} return [author_map.get(key) for key in keys]
# In contextdef get_context(): return { 'author_loader': DataLoader(load_fn=load_authors) }
# In resolver@strawberry.fieldasync def author(self, info) -> Author: loader = info.context['author_loader'] return await loader.load(self.author_id) Database Optimization
Beyond GraphQL-specific optimizations, add database indexes for fields used in GraphQL filters and ordering:
class Book(models.Model): title = models.CharField(max_length=200, db_index=True) publication_date = models.DateField(db_index=True) author = models.ForeignKey(Author, on_delete=models.CASCADE)
class Meta: indexes = [ models.Index(fields=['author', 'publication_date']), ] Use database aggregations in GraphQL resolvers:
from django.db.models import Count, Avg
@strawberry_django.type(models.Author)class Author: name: auto book_count: int = strawberry_django.field(annotate={'book_count': Count('books')}) avg_rating: float = strawberry_django.field(annotate={'avg_rating': Avg('books__rating')}) For general Django database optimization (bulk operations, efficient queries, etc.), see the Django database optimization documentation .
Caching Strategies
Cache expensive resolver computations using Djangoβs cache framework:
from django.core.cache import cache
@strawberry.fielddef featured_books(self) -> List[BookType]: cache_key = 'featured_books' cached = cache.get(cache_key) if cached is not None: return cached
books = Book.objects.filter(is_featured=True)[:10] cache.set(cache_key, books, 3600) # Cache for 1 hour return books Warning
Donβt use @lru_cache on instance methods as it can lead to memory leaks. Use Djangoβs cache framework or cached_property instead.
For cache configuration and invalidation strategies, see Djangoβs cache documentation .
Query Complexity
Limit query complexity to prevent expensive operations using Strawberryβs built-in extensions:
import strawberryfrom strawberry.extensions import QueryDepthLimiter
schema = strawberry.Schema( query=Query, extensions=[ QueryDepthLimiter(max_depth=10), # Prevent deeply nested queries ]) For custom complexity analysis and rate limiting, see Strawberry Extensions .
Pagination
Always paginate large result sets.
Offset Pagination
from strawberry_django.pagination import OffsetPaginationInput
import strawberry_djangofrom strawberry_django.pagination import OffsetPaginated
@strawberry.typeclass Query: # Use built-in pagination support books: OffsetPaginated[BookType] = strawberry_django.field(pagination=True) Tip
For production, use the built-in pagination support instead of manual slicing. See the Pagination guide for details.
Cursor Pagination (Relay)
from strawberry import relayimport strawberry_django
@strawberry.typeclass Query: books: relay.Connection[BookType] = strawberry_django.connection()
# Efficiently handles large datasets# Better for infinite scroll# Stable across data changes Monitoring and Profiling
Use Django Debug Toolbar in development to identify N+1 queries:
INSTALLED_APPS = [ 'debug_toolbar', # ...]
MIDDLEWARE = [ 'debug_toolbar.middleware.DebugToolbarMiddleware', # ...] Enable query logging to monitor database queries:
LOGGING = { 'version': 1, 'handlers': { 'console': { 'class': 'logging.StreamHandler', }, }, 'loggers': { 'django.db.backends': { 'handlers': ['console'], 'level': 'DEBUG', }, },} Best Practices
1. Always Use the Query Optimizer
# Always include the optimizer extensionschema = strawberry.Schema( query=Query, extensions=[ DjangoOptimizerExtension(), ]) 2. Paginate All List Queries
# Bad: Unbounded lists@strawberry.fielddef books(self) -> List[BookType]: return Book.objects.all() # Could return millions!
# Good: Always paginate@strawberry.fielddef books( self, pagination: OffsetPaginationInput = OffsetPaginationInput(offset=0, limit=20)) -> List[BookType]: return Book.objects.all()[pagination.offset:pagination.offset + pagination.limit] 3. Add Database Indexes
# Index fields used in filters and orderingclass Book(models.Model): title = models.CharField(max_length=200, db_index=True) publication_date = models.DateField(db_index=True)
class Meta: indexes = [ models.Index(fields=['author', 'publication_date']), ] 4. Cache Expensive Computations
from django.core.cache import cache
@strawberry.fielddef statistics(self) -> StatisticsType: cached = cache.get('statistics') if cached: return cached
stats = compute_expensive_statistics() cache.set('statistics', stats, 300) # 5 minutes return stats 5. Monitor Query Performance
Use Django Debug Toolbar in development and enable query logging to identify performance bottlenecks.
Common Patterns
Computed Fields with Annotations
from django.db.models import Count, Avg
@strawberry_django.type(models.Author)class Author: name: auto book_count: int = strawberry_django.field(annotate={'book_count': Count('books')}) avg_rating: float = strawberry_django.field(annotate={'avg_rating': Avg('books__rating')}) Model Properties with Optimization Hints
from strawberry_django.descriptors import model_propertyfrom django.db.models import Count
class Author(models.Model): name = models.CharField(max_length=100)
@model_property(annotate={'_book_count': Count('books')}) def book_count(self) -> int: return self._book_count # type: ignore Troubleshooting
Too Many Database Queries
Enable query logging to identify N+1 queries. Ensure the Query Optimizer extension is registered and youβre using strawberry_django types.
Slow Aggregations
Use database-level aggregations with annotate instead of Python-level counting:
from django.db.models import Count
# β Slow: N queriesfor author in Author.objects.all(): book_count = author.books.count()
# β
Fast: Single query with annotationauthors = Author.objects.annotate(book_count=Count('books')) Memory Issues
Always paginate large result sets. See the Pagination guide for details.
See Also
- Query Optimizer - Detailed optimizer documentation
- DataLoaders - DataLoader patterns and usage
- Pagination - Pagination strategies
- Django Database Optimization - Djangoβs optimization guide