annotate() vs aggregate()
coreintermediateaggregate() computes ONE summary value across an entire QuerySet and returns a plain dict — it is a terminal operation, not chainable further. annotate() computes a value PER OBJECT and returns a QuerySet, so every book gets its own num_authors, and that QuerySet stays fully chainable (filter(), order_by(), another annotate()). Count/Sum/Avg/Min/Max all support a default= to control what an EMPTY result returns (Count never accepts default — it always returns 0 for none).
Think of it as
aggregate() answers "what's the one number for all of this" — average price across every book, a single dict back. annotate() answers "for EACH of these, what's its number" — every book keeps its own row, with one new computed column attached to it. The clue is right there in what each returns: aggregate() gives you a dict (one summary), annotate() gives you a QuerySet (still many rows, still chainable) — reaching for the wrong one is the difference between "the average book price" and "each book, annotated with something."
What we're doing: Get the average book price as a single number, and separately annotate every author with their own book count.
- 1
- aggregate() ends the query right here, returning a dict — there is no further QuerySet to chain .filter() or .order_by() onto.
- 4
- annotate() instead returns a QuerySet where every Author keeps its own row, each with a NEW book_count attribute computed for that specific author.
Why this works: A dashboard "average order value" widget needs aggregate() — one number. A customer list showing "3 orders" next to EACH customer needs annotate() — a per-row computed value. Confusing the two either throws away the per-object detail (using aggregate() when annotate() was needed) or asks for a per-object value where only a single summary was ever wanted.
Calling aggregate() when a per-object value was actually needed
Wrong
Better
What you see: A page meant to show "3 books" next to each author instead shows one grand total for the entire site, or crashes trying to iterate a dict as if it were a list of author objects.
Why: aggregate() always collapses the entire QuerySet into ONE summary dict, regardless of how many authors exist — it has no concept of "per author" at all. annotate() is the one that keeps every object as its own row while adding a computed value alongside it, which is what a per-author display actually needs.
- aggregate()
- Computes ONE value across the whole QuerySet
- Returns a plain dict — {"price__avg": 34.35}
- Terminal — not chainable further
- annotate()
- Computes a value for EACH object
- Returns a QuerySet — every row keeps .book_count
- Still chainable — filter(), order_by(), ...
annotate() vs aggregate()
Together
Remember: aggregate() → one summary dict for the whole QuerySet (terminal, not chainable). annotate() → a per-object computed value on a QuerySet (still chainable). Sum/Avg/Min/Max return None on an empty QuerySet unless default= is given; Count always returns 0.
See also: conditional aggregation and distinct · grouping with values · f and q expressions

