Aggregation & Annotation

Everything so far has returned QuerySets of full rows — tasks, projects, tags. Sometimes what you actually want isn't the rows themselves but a number describing them: how many tasks exist in total, how many each project has, which projects are busiest. Django's ORM handles this without raw SQL through two related but distinctly different tools: aggregation and annotation. Mixing these two up is one of the most common points of confusion with the ORM, so this lesson draws the line between them clearly.

Aggregation: One Summary Value for the Whole QuerySet

Aggregation collapses an entire QuerySet down into a single summary number — a count, a sum, an average — using functions imported from django.db.models: Count, Sum, Avg, Max, and Min. You call .aggregate() on a QuerySet, and it hands back a plain dictionary with the result, not more rows.

from django.db.models import Count, Avg

# A single number: how many Task rows exist in total
Task.objects.aggregate(Count('id'))
# -> {'id__count': 47}

# Aggregation can follow a filter too — it just summarizes whatever
# QuerySet it's called on, filtered or not
Task.objects.filter(completed=True).aggregate(Count('id'))
# -> {'id__count': 31}

The key trait of .aggregate(): no matter how many rows the QuerySet started with, you get back one result describing all of them together.

Annotation: A Computed Value Added to EACH Row

Annotation uses the exact same aggregate functions, but attaches the computed value to every individual row in the QuerySet instead of collapsing them into one number. Where .aggregate() answers "what's the total?", .annotate() answers "what's this value, per row?" — you still get back a full QuerySet, just with an extra field on each object.

The clearest TaskFlow example: counting how many tasks each Project has, using the related_name='tasks' set up back in ForeignKey Relationships:

from django.db.models import Count

# Every Project, each one now carrying its own task_count attribute
projects = Project.objects.annotate(task_count=Count('tasks'))

for project in projects:
    print(project.name, project.task_count)
# Website Redesign     3
# Mobile App Launch     7
# Internal Tooling      2

Count('tasks') works here because 'tasks' is the reverse relationship name from Project to Task — Django counts, for each Project row, how many related Task rows it has, and stores that number on a new attribute named task_count. That attribute name is entirely up to you; it becomes a keyword argument on .annotate().

Filtering and Ordering by an Annotated Value

Because .annotate() still returns a normal QuerySet, the annotated value behaves like any other field — you can order by it, or filter on it. This is where annotation turns into a genuinely useful reporting tool rather than just a display detail:

# The busiest projects first — most tasks at the top
Project.objects.annotate(task_count=Count('tasks')).order_by('-task_count')

# Only projects with more than 5 tasks — filtering on the annotated value
Project.objects.annotate(task_count=Count('tasks')).filter(task_count__gt=5)

The -task_count minus sign works exactly like it does for any other order_by() field — descending order, most tasks first.

Note: This is correct, real Django ORM code — but this website's own project doesn't run a live database, so these queries aren't executed on this page. The result shapes shown above are what you'd see against a real TaskFlow database.

Why This Matters Beyond a Single Query

This combination — annotate a queryset, then order or filter by the annotated value — is genuinely one of the ORM's more powerful capabilities. It's exactly the pattern behind dashboard and reporting-style views: "top 5 busiest projects," "average priority of open tasks," "tag with the most tasks attached" — all of it expressed as a single ORM query, with no raw SQL required.

Next up: Now that you can compute values across relationships, Custom Managers & Meta Options covers how to package a frequently-reused query like this into a clean, reusable method on the model itself.