1.annotate() vs aggregate()
aggregate() 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).
Key features
- aggregate() returns a plain dict: {"price__avg": 34.35} by default, or a custom key via aggregate(average_price=Avg("price"))
- annotate() returns a QuerySet — each object gets the computed value as a real attribute (book.num_authors), and the QuerySet stays chainable
- Sum/Avg/Min/Max (not Count) accept default=... — an empty QuerySet's Sum returns None without it, or the given default with it
- Count() has no default= parameter — it always returns 0 for an empty result, never None
- Multiple aggregate functions can be combined in one aggregate() or annotate() call: aggregate(Avg("price"), Max("price"), Min("price"))
annotate() vs aggregate()
- 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(), ...
Command
Model.objects.aggregate(average=Avg("field")) # one dict
Model.objects.annotate(computed=Count("related")) # a QuerySet, per-objectaggregate() → 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.