In Django's ORM, how do you annotate each sales region with its top customer by revenue, using Subquery and OuterRef or a Window function?
answer
- correlated subquery per outer row
- OuterRef resolves against the parent
- one column, one row: values()[:1]
- RowNumber partitioned by region
- NULL sorts first descending on PostgreSQL
basics
~10 sBuild a Customer QuerySet filtered by region=OuterRef("pk"), annotated with revenue, ordered descending, reduced to values("name")[:1], and wrap it in Subquery() inside Region.annotate(). Alternatively rank customers with Window(RowNumber(), partition_by=...) and filter rank=1.
solid answer
~30 sWith `Subquery`, you write the inner query as a normal QuerySet: `Customer.objects.filter(region=OuterRef("pk")).annotate(revenue=Sum("orders__amount", default=0)).order_by("-revenue").values("name")[:1]`. `OuterRef("pk")` refers to the region in the outer query, `values("name")` leaves one column and `[:1]` one row, which a `Subquery` annotation requires; `get()` would fail because `OuterRef` cannot be resolved outside a subquery. Then `Region.objects.annotate(top_customer=Subquery(top))`. Use `default=0` or `desc(nulls_last=True)` because PostgreSQL sorts `NULL` first in descending order. The other route starts from customers: annotate revenue and `Window(RowNumber(), partition_by=[F("region")], order_by=F("revenue").desc())`, then `.filter(rank=1)`, which Django supports since 4.2. `Exists` is the related tool for yes/no questions.
code
python · 16 linesfrom django.db.models import Exists, OuterRef, Subquery, Sum
top = (
Customer.objects.filter(region=OuterRef("pk"))
.annotate(revenue=Sum("orders__amount", default=0))
.order_by("-revenue")
)
big_orders = Order.objects.filter(
customer__region=OuterRef("pk"), amount__gte=50000
)
regions = Region.objects.annotate(
top_customer=Subquery(top.values("name")[:1]),
top_revenue=Subquery(top.values("revenue")[:1]),
has_big_order=Exists(big_orders),
)go deeper
Recall that Subquery wraps a QuerySet and OuterRef points from the inner query at a field of the outer row.
Explain the one-column, one-row rule for a Subquery annotation and why get() cannot be used inside it.
Choose between Subquery and Window for greatest-per-group, handle NULL ordering and ties, and use Exists for yes/no filters.
Decide whether per-group leaders belong in live queries or in precomputed rollups, weighing freshness against the cost on large tables.
## The problem: greatest value per group "Revenue per region" is a `GROUP BY`. "The **top customer** in each region" is harder, because a plain aggregate gives you the maximum **value**, not the **row** that holds it. `Max("customers__orders__amount")` tells you the largest order, not who placed it. Django's ORM offers two tools for this: a **correlated subquery** built with `Subquery` and `OuterRef`, and a **window function** built with `Window`. ## Route 1: `Subquery` and `OuterRef` ```python from django.db.models import OuterRef, Subquery, Sum top = ( Customer.objects.filter(region=OuterRef("pk")) .annotate(revenue=Sum("orders__amount", default=0)) .order_by("-revenue") .values("name")[:1] ) regions = Region.objects.annotate(top_customer=Subquery(top)) ``` How the pieces work: - **`OuterRef("pk")`** acts like an `F()` expression that points at the **outer** query's row; it is resolved only when the inner QuerySet is embedded in the outer one. - The inner QuerySet is ordinary Django: filter, annotate, order. Its `Sum` is grouped per customer. - **`values("name")`** reduces it to **one column** and **`[:1]`** to **one row** (a `LIMIT 1`), the shape a scalar subquery in `SELECT` must have. - `get()` or `first()` cannot be used: they would try to run the inner query immediately, before `OuterRef` has anything to point at. - Return two facts (name and revenue) with two `Subquery` annotations, or annotate the customer's `pk` and fetch details separately. Watch the **`NULL` ordering**: a customer with no orders has a `NULL` sum, and PostgreSQL places `NULL`s **first** in descending order, so an inactive customer could "win". Either sum with `default=0`, as above, or order with `F("revenue").desc(nulls_last=True)`. ## Route 2: `Window` with `RowNumber` Start from customers instead and rank them inside each region: ```python from django.db.models import F, Sum, Window from django.db.models.functions import RowNumber ranked = ( Customer.objects.annotate( revenue=Sum("orders__amount", default=0), rank=Window( expression=RowNumber(), partition_by=[F("region")], order_by=F("revenue").desc(), ), ) .filter(rank=1) .select_related("region") ) ``` - **`Window(expression, partition_by=None, order_by=None, frame=None)`** produces `... OVER (PARTITION BY ... ORDER BY ...)`; the docs recommend wrapping column names in `F()` for `partition_by`. - **`RowNumber`** gives exactly one row per region even with ties; **`Rank`** or **`DenseRank`** return every tied customer. - **Filtering on a window annotation** (`.filter(rank=1)`) works since **Django 4.2**; Django wraps the query so the filter runs after the window is computed. Disjunctive (`OR`) filters that mix a window with other conditions on an aggregating query raise `NotImplementedError`. ## Reading the SQL of the `Subquery` route Printing `str(regions.query)` shows roughly this shape, which is worth being able to explain: 1. The outer `SELECT` lists the region columns plus one parenthesised subquery per `Subquery` annotation. 2. Inside, `WHERE U0.region_id = (region.id)` is the `OuterRef("pk")` correlation. 3. `GROUP BY U0.id` comes from the inner `annotate(revenue=Sum(...))`, grouping orders per customer. 4. `ORDER BY ... DESC LIMIT 1` comes from `order_by("-revenue")` and the `[:1]` slice. Two separate `Subquery` annotations (name and revenue) repeat that inner query; the database may or may not optimise the repetition, which is one reason to prefer the `Window` route when you need several columns of the winner. ## Choosing between them | | `Subquery` + `OuterRef` | `Window` + `RowNumber` | |---|---|---| | Starts from | `Region` rows | `Customer` rows | | Result | every region, `None` if no customers | only regions that have customers | | Extra columns of the winner | one `Subquery` per column | the whole `Customer` object | | Ties | whichever row sorts first | explicit: `RowNumber` vs `Rank` | | Cost | one inner query per outer row, in principle | one pass with a sort per partition | Neither is always faster; check the plan on real data. ## `Exists` for yes/no questions When you only need to know **whether** a related row exists, use **`Exists(queryset)`**, a `Subquery` subclass that emits `EXISTS (...)` and lets the database stop at the first match. It can annotate (`has_big_order=Exists(...)`), filter directly (`Region.objects.filter(Exists(big_orders))`) and be negated with `~Exists(...)` for `NOT EXISTS`. Django removes ordering inside it automatically, and it needs no `values()` because the columns are discarded.
- In Django, why can't the inner query use .get() or .first() instead of values()[:1]?`get()` and `first()` evaluate the QuerySet immediately, but `OuterRef` can only be resolved once the QuerySet is embedded in the outer query. The slice keeps it lazy and becomes `LIMIT 1` inside the subquery.
- In Django, how do you filter regions that have at least one order over 50,000 without adding a column?Pass the `Exists` expression straight to `filter()`: `Region.objects.filter(Exists(Order.objects.filter(customer__region=OuterRef("pk"), amount__gte=50000)))`. Used as a filter it is not added to the `SELECT` list, and `~Exists(...)` gives the opposite set.
- In Django, what changes if two customers in a region tie for top revenue?With the `Subquery`, the database returns whichever tied row sorts first, which is arbitrary unless you add a tie-breaker such as `order_by("-revenue", "pk")`. With `Window`, `RowNumber` still picks one row, while `Rank` gives both rank 1 and the filter returns both.
saying these in an interview costs you the question
- OuterRef can be used in any QuerySet, not just inside a Subquery
- A Subquery annotation may return several columns if you list them in values()
- Max("customers__orders__amount") tells you which customer is on top
- Window annotations cannot be filtered in any Django version
- Exists needs values() to reduce it to one column