Every Django developer is familiar with .annotate(). It’s the go-to tool for adding computed properties to your database queries. But what happens when you only need to calculate a value to filter or order your results, without actually displaying it?
Before Django 3.2, you had no choice but to use annotate(), which forced the database to select and return those computed fields anyway, wasting bandwidth. Enter QuerySet.alias(). This tutorial breaks down how to master this performance-saving feature in 10 easy steps.
Step 1: Understand the Overhead of annotate()
Traditionally, if you wanted to filter books that have grossed over $10,000, you would write:
Book.objects.annotate(total_sales=Sum('sales__amount')).filter(total_sales__gt=10000)
This forces the database to include total_sales in the SQL SELECT statement, even if your Python template only needs the book's title.
Step 2: Swap annotate() with alias()
If you don't need the computed value in your final Python objects, replace annotate() with alias(). It stores the expression temporarily on the query without selecting it:
from django.db.models import Sum
# This defines the expression but won't fetch it yet
queryset = Book.objects.alias(total_sales=Sum('sales__amount'))
Step 3: Filter Querysets Using Your Alias
Once declared, you can use your defined alias immediately inside .filter() or .exclude().
high_performers = Book.objects.alias(
total_sales=Sum('sales__amount')
).filter(total_sales__gt=10000)
Behind the scenes, Django places the calculation inside a SQL HAVING or WHERE clause instead of polluting the SELECT clause.
Step 4: Use Your Alias for Ordering
You can also use an alias to sort your queryset. For instance, if you want to sort your books by total sales without fetching the total sales value:
sorted_books = Book.objects.alias(
total_sales=Sum('sales__amount')
).order_by('-total_sales')
Step 5: Combine Multiple Aliases for Complex Queries
Just like annotations, you can chain multiple aliases together to build highly expressive queries:
from django.db.models import Count, F
popular_books = Book.objects.alias(
review_count=Count('reviews'),
sales_count=Count('sales')
).filter(review_count__gt=50, sales_count__gt=100)
Step 6: Chain Aliases to Annotations
You can use an alias as a building block for an annotation. This keeps complex formulas readable:
from django.db.models import Value
from django.db.models.functions import Concat
# Let's combine first name and last name as an alias, then annotate a custom greeting
authors = Author.objects.alias(
full_name=Concat('first_name', Value(' '), 'last_name')
).annotate(
greeting=Concat(Value('Welcome back, '), 'full_name')
)
Step 7: Keep Your Python Objects Lightweight
When you retrieve the objects, notice that the alias is not present as an attribute on your model instances.
book = Book.objects.alias(total_sales=Sum('sales__amount')).first()
# This will raise an AttributeError!
print(book.total_sales)
This is intentional! It saves Django from having to instantiate, map, and cache extra fields in your Python RAM.
Step 8: Reuse the Same QuerySet Safely
Because aliases don’t change the shape of the returned model instances, you can safely pass an aliased queryset to other methods or APIs that expect a standard, unmodified queryset.
Step 9: Know When to Opt for annotate() Instead
If your frontend, API serializer, or Django template actually needs to render the calculated value (e.g., displaying "Total Sales: $12,400" next to the book title), use annotate(). Use alias() strictly for internal filtering, excluding, or sorting logic.
Step 10: Inspect the Resulting SQL
To truly appreciate alias(), compare the generated SQL in your shell. You will notice that the aliased expressions are cleanly tucked away inside subqueries or clauses, without cluttering the main SELECT statement:
print(str(queryset.query))
By leveraging QuerySet.alias(), you write cleaner SQL, reduce database payload sizes, and gain fine-grained control over what gets pulled into Python memory. Try swapping your filters over to aliases today!
Comments