Django QuerySet Annotate

Adding calculated fields to each object in your QuerySet

🏷️ What is annotate()?

The annotate() method adds calculated fields to each object in a QuerySet. Unlike aggregate(), it returns a QuerySet where each object has the new computed value attached.


# Add review count to each book
from django.db.models import Count
from myapp.models import Book

books = Book.objects.annotate(review_count=Count('reviews'))
print(books[0].review_count)
                                    

Output:

12

Key annotate() Features

Per-Object Values

Adds calculated field to each object

books = Book.objects.annotate(
    num_reviews=Count('reviews')
)
🔗

Returns QuerySet

Can chain with other methods

books = Book.objects.annotate(
    num_reviews=Count('reviews')
).filter(num_reviews__gt=10)
🔢

Aggregate Functions

Use Sum, Avg, Count, etc.

authors = Author.objects.annotate(
    avg_rating=Avg('books__rating')
)
🎯

Related Objects

Calculate across relationships

authors = Author.objects.annotate(
    book_count=Count('books')
)

🔹 Basic annotate() Usage

Use annotate() to add calculated fields to each object in your QuerySet. The new field becomes accessible as an attribute on each object, just like regular model fields.

# models.py
from django.db import models

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, related_name='books')
    price = models.DecimalField(max_digits=6, decimal_places=2)

class Review(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='reviews')
    rating = models.IntegerField()

# views.py
from django.db.models import Count
from myapp.models import Book

# Add review count to each book
books = Book.objects.annotate(review_count=Count('reviews'))

for book in books:
    print(f"{book.title}: {book.review_count} reviews")

Output:

Python Guide: 12 reviews

Django Basics: 8 reviews

Web Development: 15 reviews

🔹 Count with annotate()

Count() is commonly used with annotate() to count related objects. This is perfect for showing how many reviews a book has, how many books an author wrote, or similar relationships.

from django.db.models import Count
from myapp.models import Author, Book

# Count books per author
authors = Author.objects.annotate(book_count=Count('books'))

for author in authors:
    print(f"{author.name}: {author.book_count} books")

# Count reviews per book
books = Book.objects.annotate(
    review_count=Count('reviews')
).order_by('-review_count')

# Filter by annotated field
popular_books = Book.objects.annotate(
    review_count=Count('reviews')
).filter(review_count__gte=10)

print(f"Popular books: {popular_books.count()}")

Output:

John Doe: 5 books

Jane Smith: 3 books

Popular books: 7

🔹 Sum with annotate()

Use Sum() with annotate() to calculate totals for each object. This is useful for calculating total revenue per author, total pages per category, or any cumulative values.

from django.db.models import Sum
from myapp.models import Author

# Calculate total pages written by each author
authors = Author.objects.annotate(
    total_pages=Sum('books__pages')
)

for author in authors:
    print(f"{author.name}: {author.total_pages} total pages")

# Calculate total revenue per author
authors = Author.objects.annotate(
    total_revenue=Sum('books__price')
)

# Find most prolific authors
top_authors = Author.objects.annotate(
    total_pages=Sum('books__pages')
).order_by('-total_pages')[:5]

for author in top_authors:
    print(f"{author.name}: {author.total_pages} pages")

Output:

John Doe: 1450 total pages

Jane Smith: 890 total pages

Bob Wilson: 2100 pages

🔹 Avg with annotate()

Use Avg() with annotate() to calculate average values for each object. Perfect for finding average ratings per book, average price per author, or mean values across relationships.

from django.db.models import Avg
from myapp.models import Book, Author

# Calculate average rating for each book
books = Book.objects.annotate(
    avg_rating=Avg('reviews__rating')
)

for book in books:
    print(f"{book.title}: {book.avg_rating:.1f} stars")

# Calculate average book price per author
authors = Author.objects.annotate(
    avg_book_price=Avg('books__price')
)

# Find highly rated books
top_rated = Book.objects.annotate(
    avg_rating=Avg('reviews__rating')
).filter(avg_rating__gte=4.5).order_by('-avg_rating')

for book in top_rated:
    print(f"{book.title}: {book.avg_rating:.1f} stars")

Output:

Python Guide: 4.8 stars

Django Basics: 4.5 stars

Web Development: 4.7 stars

🔹 Multiple Annotations

You can add multiple annotated fields in a single query. This is efficient and lets you calculate several metrics at once for each object in your QuerySet.

from django.db.models import Count, Avg, Sum
from myapp.models import Author

# Multiple annotations at once
authors = Author.objects.annotate(
    book_count=Count('books'),
    avg_rating=Avg('books__reviews__rating'),
    total_pages=Sum('books__pages'),
    total_revenue=Sum('books__price')
)

for author in authors:
    print(f"{author.name}:")
    print(f"  Books: {author.book_count}")
    print(f"  Avg Rating: {author.avg_rating:.1f}")
    print(f"  Total Pages: {author.total_pages}")
    print(f"  Revenue: ${author.total_revenue}")

Output:

John Doe:

Books: 5

Avg Rating: 4.6

Total Pages: 1450

Revenue: $174.95

🔹 Filtering on Annotations

After annotating, you can filter based on the calculated values. This powerful technique lets you find objects that meet criteria based on aggregated data from related objects.

from django.db.models import Count, Avg
from myapp.models import Author, Book

# Find authors with more than 3 books
prolific_authors = Author.objects.annotate(
    book_count=Count('books')
).filter(book_count__gt=3)

# Find books with high average ratings
highly_rated = Book.objects.annotate(
    avg_rating=Avg('reviews__rating')
).filter(avg_rating__gte=4.5)

# Complex filtering
popular_authors = Author.objects.annotate(
    book_count=Count('books'),
    avg_rating=Avg('books__reviews__rating')
).filter(
    book_count__gte=3,
    avg_rating__gte=4.0
)

for author in popular_authors:
    print(f"{author.name}: {author.book_count} books, {author.avg_rating:.1f} rating")

Output:

John Doe: 5 books, 4.6 rating

Jane Smith: 4 books, 4.3 rating

🔹 annotate() vs aggregate()

Understanding the difference between annotate() and aggregate() is crucial. annotate() adds fields to each object and returns a QuerySet, while aggregate() calculates a single value across all objects and returns a dictionary.

from django.db.models import Count, Avg
from myapp.models import Book

# annotate() - adds field to each object
books_with_counts = Book.objects.annotate(
    review_count=Count('reviews')
)
# Returns QuerySet, can iterate
for book in books_with_counts:
    print(f"{book.title}: {book.review_count}")

# aggregate() - single value for all objects
total_stats = Book.objects.aggregate(
    total_reviews=Count('reviews'),
    avg_rating=Avg('reviews__rating')
)
# Returns dictionary
print(f"Total reviews: {total_stats['total_reviews']}")
print(f"Overall avg: {total_stats['avg_rating']:.1f}")

Output:

Python Guide: 12

Django Basics: 8

Total reviews: 35

Overall avg: 4.4

🧠 Test Your Knowledge

What does annotate() return?