Django select_related() vs prefetch_related(): SQL explained

Django select_related() vs prefetch_related(): SQL Explained

Django select_related() or prefetch_related()? Both methods eliminate N+1 queries in Django, but they produce very different SQL. The first adds a join to the main query. The second runs multiple queries, then connects the objects in Python. The right choice depends less on data volume than on the type of relationship being traversed. To understand why, let’s start with a deliberately simple model: from django.conf import settings from django.db import models class Post(models.Model): title = models.CharField(max_length=200) author = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="posts", ) tags = models.ManyToManyField("Tag", related_name="posts") class Comment(models.Model): post = models.ForeignKey( Post, on_delete=models.CASCADE, related_name="comments", ) body = models.TextField() is_public = models.BooleanField(default=True) author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) class Tag(models.Model): name = models.CharField(max_length=50) From a Post, author refers to at most one user. By contrast, comments and tags can contain multiple objects. This difference in cardinality almost always determines which method to use. ...

July 20, 2026 · 8 min · Anthony
Renaming Django ORM fields with F() in values()

Renaming Django ORM fields with F() in values()

When exposing data from a Django model to an API or serializer, the model’s field names don’t always match what you want to return. The usual approach: fetch instances, then rename in Python. There’s a better option: let the database do the work using F() inside values(). The problem: model field names dictate output class Task(models.Model): name = models.CharField(...) created_at = models.DateTimeField(...) If you want to return task_name instead of name, the typical approach is to fetch the data and rename in Python, either with a dict comprehension or inside the serializer. Either way, the transformation happens after the fact, in memory. ...

May 5, 2026 · 2 min · Anthony
Django Window Functions vs GROUP BY: Chainable QuerySets

Django Window Functions vs GROUP BY: Chainable QuerySets

Django ORM gives you two ways to add a computed value across a set of rows: annotate() with a classic aggregation (Max, Count, Sum…) or annotate() with a Window function. On the surface they look similar. In practice, they behave in fundamentally different ways — and picking the wrong one can break your entire filtering chain. GROUP BY with annotate(): rows that collapse When you combine values() and annotate() with an aggregation, Django generates a GROUP BY in SQL. The result: rows get merged, and you end up with one row per group. ...

May 4, 2026 · 4 min · Anthony
Django in_bulk(): why it beats filter() for bulk lookups

Django in_bulk(): why it beats filter() for bulk lookups

When you have a list of identifiers and want to retrieve the corresponding instances, the usual reflex in Django is filter(pk__in=[...]). It works — one SQL query. But in_bulk() is an often-overlooked ORM optimization: it returns a dictionary {id: instance} instead of a QuerySet, which fundamentally changes how you access results. Where filter() forces an O(n) traversal to find an object by ID, in_bulk() gives direct O(1) access. in_bulk() signature and behavior QuerySet.in_bulk(id_list=(), *, field_name='pk') id_list: list of identifiers to retrieve. If omitted (called without arguments), returns all objects in the table. field_name: field used as the dictionary key. Must have unique=True, otherwise Django raises a ValueError. The generated SQL is a simple WHERE pk IN (...) clause — one query regardless of list size. ...

May 4, 2026 · 4 min · Anthony

Newsletter

Get new articles delivered straight to your inbox.

No spam. Unsubscribe in one click.