Q, F, annotate and aggregate

Django · Practical · 15 min

Reports, counters and complex filters belong in the database, not in Python loops.

Q objects combine conditions with | (OR), & (AND) and ~ (NOT):

Post.objects.filter(Q(status="pub") | Q(author__name="Ann"), ~Q(views=0))

F expressions refer to a column's value inside the query:

  • compare columns: filter(updated__gt=F("created"))
  • update atomically: Post.objects.filter(pk=pk).update(views=F("views") + 1) runs views = views + 1 in SQL, with no race between concurrent requests. Doing post.views += 1; post.save() loses increments under load.

`aggregate()` summarizes a whole QuerySet into a dict: Post.objects.aggregate(total=Sum("views"), avg=Avg("views")) returns {"total": 15, "avg": 5.0}.

`annotate()` adds a computed value to each row:

Author.objects.annotate(n=Count("posts")).filter(n__gte=5).order_by("-n")

Annotations can be filtered and ordered like fields, and conditional aggregation counts subsets: Count("posts", filter=Q(posts__status="pub")).

values("field").annotate(...) turns into GROUP BY field, the ORM's way of grouping: Post.objects.values("author__name").annotate(total=Sum("views")).

Database functions (Lower, Coalesce, TruncMonth, Concat, ...) and Case/When work anywhere expressions do. For correlated queries, Subquery(...) with OuterRef("pk") and Exists(...) express "latest comment per post" or "has any unapproved comment" in a single SQL statement.

Beware combining two multi-valued Counts in one annotate: the joins multiply rows. Use distinct=True or subqueries.

Example

from django.db.models import Avg, Count, Exists, F, OuterRef, Q, Sum
from django.db.models.functions import TruncMonth
from blog.models import Author, Comment, Post

popular_or_mine = Post.objects.filter(Q(views__gte=100) | Q(author__name="Ann"), ~Q(status="draft"))

Post.objects.filter(pk=1).update(views=F("views") + 1)       # race-free counter

Post.objects.aggregate(total=Sum("views"), avg=Avg("views"))  # {'total': 15, 'avg': 5.0}

authors = (Author.objects
           .annotate(n_posts=Count("posts"),
                     n_published=Count("posts", filter=Q(posts__status="pub")))
           .filter(n_posts__gte=1)
           .order_by("-n_posts"))
# [(a.name, a.n_posts, a.n_published) for a in authors] -> [('Ann', 2, 1), ('Bo', 1, 1)]

per_author = Post.objects.values("author__name").annotate(total=Sum("views"))  # GROUP BY
per_month = Post.objects.annotate(m=TruncMonth("created")).values("m").annotate(n=Count("id"))

pending = Comment.objects.filter(post=OuterRef("pk"), approved=False)
needs_review = Post.objects.filter(Exists(pending))

Remember

  • Q objects build OR/NOT conditions; F references columns inside SQL
  • update(x=F("x") + 1) avoids lost updates
  • aggregate() returns one dict; annotate() adds a value per row
  • values(...).annotate(...) is GROUP BY

Watch out for

  • Incrementing counters in Python with obj.x += 1; obj.save(), which loses concurrent updates.

Check yourself

  1. What does Author.objects.annotate(n=Count("posts")) return?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.djangoproject.com/en/6.0/topics/db/aggregation/