Development Long read Django
Are your Django queries slow?

Are your Django queries slow?

QuerySet lazy evaluation, join strategies, and profiling tools.

29 January 2026 22 min read
Share
X in

Introduction

Django ORM lets you work with the database without writing SQL, but misuse can generate hundreds of unnecessary queries per request. QuerySets are lazy: no SQL runs until evaluation. That delay is powerful but accessing related objects in a loop causes the N+1 problem.

This guide covers QuerySet optimization techniques, profiling with Django Debug Toolbar and connection.queries, and safe pagination strategies on large tables.

QuerySet Basics and Lazy Evaluation

Post.objects.filter(status='published') does not produce SQL yet. list(), len(), iteration, or slicing triggers the query. queryset.count() runs a separate COUNT query; prefer count() over len(queryset).

  • all(), filter(), exclude(), order_by() return new QuerySets
  • values() and values_list() return dicts/tuples
  • exists() and count() are optimized queries
qs = Post.objects.filter(status='published')  # no SQL
for post in qs:
    print(post.title)  # single SELECT

# Bad: separate query per post
for post in Post.objects.all():
    print(post.author.username)  # N+1!

The N+1 Problem and Detection

N+1 means one query fetches main rows and N queries fetch related data per row. Listing 100 posts and showing author names can cause 101 queries. Monitor connection.queries length with django-debug-toolbar or logging.

# Bad
posts = Post.objects.all()
for p in posts:
    print(p.author.email)  # SELECT each iteration

# Good
posts = Post.objects.select_related('author')
for p in posts:
    print(p.author.email)  # single JOIN
If your template uses {{ post.author.name }}, plan select_related in the view.

select_related: ForeignKey and OneToOne

select_related uses SQL JOIN; it fetches ForeignKey and OneToOne relations in one query. Chain relations with double underscores: select_related('author__profile').

Post.objects.select_related(
    'author',
    'category',
).filter(status='published')

# In CBV
class PostDetailView(DetailView):
    queryset = Post.objects.select_related('author', 'category')

prefetch_related: ManyToMany and Reverse FK

prefetch_related runs separate queries and joins in Python memory. Use for ManyToMany and reverse ForeignKey. Prefetch() lets you filter the sub-QuerySet.

  • select_related: JOIN, single query
  • prefetch_related: separate queries + in-memory join
  • Both can be used together
from django.db.models import Prefetch

Post.objects.prefetch_related(
    'tags',
    Prefetch(
        'comments',
        queryset=Comment.objects.filter(is_approved=True),
    ),
)

only, defer, and annotate

only() and defer() load or exclude specific fields; useful for large TEXT/BLOB columns. annotate() and aggregate() compute at SQL level, avoiding Python loops.

from django.db.models import Count, Avg

Post.objects.annotate(
    comment_count=Count('comments'),
    avg_rating=Avg('comments__rating'),
).filter(comment_count__gte=5)

Post.objects.only('id', 'title', 'slug').defer('body')

Profiling and Production Tips

Use django-debug-toolbar SQL panel in development. Monitor slow endpoints in production with Sentry or APM. iterator(chunk_size=2000) reduces memory for large exports.

  1. Preload required relations in the view
  2. Use pagination with LIMIT/OFFSET
  3. Define indexes in migrations
  4. Use raw SQL only on measured bottlenecks
for row in Post.objects.iterator(chunk_size=500):
    export_row(row)

Conclusion

ORM convenience does not require a performance penalty; the right QuerySet chain keeps code readable and fast. Do not optimize without measuring; sometimes cache beats an extra JOIN.

  • Choose select_related/prefetch_related based on template needs
  • exists() is faster than count() for empty checks
  • Use bulk_create and bulk_update for batch operations