skip to content

QuerySet API

The QuerySet and Manager API for reading and writing rows: lazy evaluation, lookups with Q and F, bulk writes, aggregation and raw SQL. Interviewers probe what SQL each call emits.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

29

In Django's ORM, what is the difference between aggregate() and annotate(), and what does each one return?

level: juniorimportance: must knowfreq 70%

answer

  1. one summary versus one value per row
  2. a dict versus a QuerySet
  3. terminal versus chainable
  4. default alias field__function

basics

~10 s

aggregate() computes summary values over the whole QuerySet and immediately returns a dict. annotate() adds a computed value to every object and returns a new, lazy QuerySet you can keep filtering and ordering.

solid answer

~30 s

`aggregate()` is terminal: `Order.objects.aggregate(total=Sum("amount"))` runs the query at once and returns a dict such as `{"total": Decimal("1830.00")}`; without a name the key is the default alias, `amount__sum`. `annotate()` is per object: `Region.objects.annotate(n_customers=Count("customers"))` returns a QuerySet in which each `Region` carries an `n_customers` attribute, grouped by the model's primary key, and you can still `filter()`, `order_by()` or even `aggregate()` over that annotation. Both accept the same aggregate classes (`Count`, `Sum`, `Avg`, `Min`, `Max`...). On an empty set `Sum` and friends return `None` unless you pass `default=`, while `Count` returns `0`.

code

python · 14 lines
python
from django.db.models import Count, Sum

# Terminal: returns a dict right away.
totals = Order.objects.filter(status="paid").aggregate(
    revenue=Sum("amount", default=0), orders=Count("id")
)
# {'revenue': Decimal('1830.00'), 'orders': 6}

# Lazy: a QuerySet of Region objects, each with n_customers.
busy = (
    Region.objects.annotate(n_customers=Count("customers"))
    .filter(n_customers__gte=10)
    .order_by("-n_customers")
)

go deeper

for a junior

Recall the shapes: aggregate() returns a dict with one summary, annotate() returns a QuerySet with a value on every object.

for a middle

Explain laziness and SQL: aggregate() executes at once, annotate() adds GROUP BY on the primary key, and filters on annotations become HAVING.

for a senior

Spot the Python loop of per-row count() calls that one annotate() replaces, and guard reports against None from empty aggregates with default=.

for a principal

Decide which reporting numbers belong in live ORM annotations and which in precomputed tables, based on data volume and how fresh the numbers must be.

## Two methods, two shapes of result Django's ORM offers two ways to ask the database for computed values: - **`aggregate(*args, **kwargs)`** computes one or more summary values over **the whole QuerySet** and returns a **plain Python dict**. It is **terminal**: calling it executes the SQL immediately, just like `count()` or `list()`. - **`annotate(*args, **kwargs)`** adds a computed value **to every object** in the QuerySet and returns a **new QuerySet**. It is **lazy** and **chainable**: nothing runs until you iterate, and you can keep adding `filter()`, `order_by()` or `values()`. | | `aggregate()` | `annotate()` | |---|---|---| | Returns | `dict` | `QuerySet` | | Rows in the result | one summary | one per object (or per group with `values()`) | | Executes | immediately | when evaluated | | SQL shape | `SELECT SUM(...) FROM ...` | `SELECT ..., SUM(...) ... GROUP BY <pk>` | | Can be chained | no | yes | Both take the same **aggregate expressions** from `django.db.models`, such as `Count`, `Sum`, `Avg`, `Min`, `Max`, `StdDev`, `Variance` and, since Django 6.0, `AnyValue`. ## `aggregate()` in practice ```python from django.db.models import Avg, Sum Order.objects.filter(status="paid").aggregate(Sum("amount")) # {'amount__sum': Decimal('1830.00')} Order.objects.filter(status="paid").aggregate(total=Sum("amount"), avg=Avg("amount")) # {'total': Decimal('1830.00'), 'avg': Decimal('305.00')} ``` Key points: - Without a keyword, the dict key is the **default alias** `<field>__<function>`, e.g. `amount__sum`. - A complex expression such as `Sum(F("amount") * 2)` has no default alias, so you must name it; otherwise Django raises `TypeError`. - Because the result is a dict, you cannot chain `.filter()` after it. ## `annotate()` in practice ```python from django.db.models import Count regions = Region.objects.annotate(n_customers=Count("customers")) for region in regions: print(region.name, region.n_customers) ``` Here Django joins `Customer`, groups by the `Region` primary key and exposes `n_customers` on each instance. The annotation behaves like a read-only field on those objects: - **filter on it**: `.filter(n_customers__gte=10)` becomes a `HAVING` clause; - **order by it**: `.order_by("-n_customers")`; - **aggregate over it**: `.aggregate(Avg("n_customers"))` averages the per-region counts; - an annotation name that clashes with a model field raises `ValueError`; - `alias()` works like `annotate()` but does not add the value to the `SELECT` list, which is handy when you only filter on it. ## `annotate()` is not only for aggregates `annotate()` accepts any expression, not just aggregates, and the two kinds behave differently: - a **non-aggregate** annotation such as `annotate(net=F("amount") - F("discount"))` or `annotate(month=TruncMonth("placed_at"))` computes a value per row and adds **no** `GROUP BY`; - an **aggregate** annotation such as `Count("customers")` makes Django group by the model's primary key (or by the fields named in an earlier `values()`); - you can mix both, and later annotations can reference earlier ones: `annotate(n=Count("orders")).annotate(big=Case(When(n__gte=10, then=True), default=False))`. `aggregate()` has no such split: everything in it is summarised into the single dict, and a non-aggregate expression there raises `TypeError` because it is not an aggregate. ## Empty sets and `None` Aggregates on no rows return SQL `NULL`, which Django turns into `None`: - `Order.objects.none().aggregate(Sum("amount"))` returns `{'amount__sum': None}`; - `Sum("amount", default=0)` returns `0` instead; the `default` argument wraps the aggregate in `Coalesce`; - **`Count` always returns `0`** for an empty set or group and does not accept `default` at all. ## Interview pitfalls - Saying `aggregate()` can be followed by `.filter()`: it returns a dict, so filtering must come **before** it. - Assuming a `Sum` total is never `None`: empty inputs give `None` unless `default=` is set, which breaks arithmetic in templates and serializers. - Forgetting that an `annotate()` with an aggregate changes the SQL to a grouped query, which matters once a second multi-valued relation is joined. - Reaching for `annotate()` when one number is needed; the per-row query does more work than a single `aggregate()`. ## Which one to reach for 1. You need **one number** (total revenue, average order value) for a report: `aggregate()`. 2. You need **a number per row** (orders per customer, revenue per region) to display in a list or to filter by: `annotate()`. 3. You need **a number per group of rows** that are not model instances (revenue per month): `values(...)` followed by `annotate()`, which is covered by the GROUP BY question. A common junior mistake is to loop over customers in Python calling `customer.orders.count()` for each; `annotate()` gets all the counts in one query.

  • In Django, can you call aggregate() on top of an annotate()?
    Yes. `Region.objects.annotate(n=Count("customers")).aggregate(Avg("n"))` first computes a count per region, then averages those counts, returning a dict like `{'n__avg': 12.5}`. The aggregate can reference any alias defined by an earlier `annotate()`.
  • In Django, why does Order.objects.aggregate(Sum(F("amount") * 2)) raise TypeError?
    `aggregate()` needs a key for each value. A plain `Sum("amount")` has the default alias `amount__sum`, but an expression over several parts has none, so Django raises `TypeError` asking for an alias. Pass it as a keyword: `aggregate(double=Sum(F("amount") * 2))`.

saying these in an interview costs you the question

  • aggregate() returns a QuerySet you can keep filtering
  • annotate() runs its query as soon as it is called
  • Sum() on an empty QuerySet returns 0 by default
  • annotate() collapses the QuerySet into a single row
  • Filtering on an annotation is done in Python after fetching
open as a page

In Django's ORM, what does it mean that a QuerySet is lazy, and which operations make it actually query the database?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Building or chaining a QuerySet only composes a query and returns a new QuerySet. SQL runs when Python needs rows: iteration, list(), len(), bool() or an if test, repr(), a slice with a step, or pickling.

open as a page

In Django's ORM, what is the difference between filter() and get(), and which exceptions can get() raise?

level: juniorimportance: must knowfreq 75%

basics

~10 s

filter() returns a lazy QuerySet of zero or more rows and never raises for no matches. get() runs immediately and returns exactly one instance, raising Model.DoesNotExist for none and Model.MultipleObjectsReturned for more than one.

open as a page

In Django, when do you use Manager.raw() rather than connection.cursor(), and what does each one give you back?

level: juniorimportance: must knowfreq 55%

basics

~20 s

Manager.raw() runs a SELECT and returns a lazy RawQuerySet of model instances, matched to fields by column name, so the primary key must be selected. connection.cursor() bypasses models: it runs any statement and returns plain tuples.

open as a page

A Django dashboard view runs the same SELECT four times; how does the QuerySet result cache explain it, and what fixes it?

level: middleimportance: must knowfreq 60%

basics

~20 s

Each QuerySet caches its own rows after its first full evaluation, but helpers, .all(), filter() and template lookups like user.orders.all build new QuerySets with empty caches. Evaluate one QuerySet once and reuse that object everywhere.

open as a page

In Django's ORM, how do you express OR and NOT conditions with Q objects, and how do they combine with keyword arguments in filter()?

level: middleimportance: must knowfreq 65%

basics

~20 s

Keyword arguments in filter() are always ANDed, so OR and NOT need Q objects: | for OR, ~ for NOT, & for AND, ^ for XOR. Q objects are passed positionally, before any keyword arguments, and are ANDed with them.

open as a page

In Django, why define a published() filter on a custom QuerySet exposed with as_manager() or Manager.from_queryset() instead of only on a Manager?

level: middleimportance: must knowfreq 58%

basics

~20 s

A Manager-only method is lost once filter() returns a plain QuerySet, so it cannot be chained. Defining published() on a QuerySet subclass exposed with as_manager() or from_queryset() makes it callable on the manager and on every QuerySet.

open as a page

In Django's ORM, how do get_or_create() and update_or_create() differ, and how does each behave when two requests race to create the same row?

level: middleimportance: must knowfreq 62%

basics

~20 s

get_or_create() fetches a row by the lookup kwargs or inserts one; update_or_create() also writes defaults onto a found row. Both return (object, created) and survive a race only when a database unique constraint covers the lookup fields.

open as a page

In Django, why does Region.objects.annotate(reps=Count("sales_reps"), revenue=Sum("customers__orders__amount")) return inflated numbers, and how do you fix it?

level: seniorimportance: must knowfreq 55%

basics

~10 s

Both aggregates hang off one SQL query that joins two independent multi-valued relations, so each region's rows are multiplied (reps × orders) before grouping. Count(distinct=True) repairs the count; the Sum needs its own Subquery.

open as a page

In a Django code review you find Person.objects.raw(f"SELECT * FROM myapp_person WHERE last_name = '{name}'"); what is wrong, and how do you fix it?

level: seniorimportance: must knowfreq 60%

basics

~20 s

The f-string splices user input into the SQL text, so a crafted name rewrites the query. Pass it through params with an unquoted placeholder: raw("... WHERE last_name = %s", [name]); the driver then sends it as data.

open as a page

When two Django requests register for the last seat of a workshop at the same moment and both succeed, why does it happen and how do update() and F() fix it?

level: seniorimportance: must knowfreq 50%

basics

~10 s

Both requests read seats_taken before either writes, so both pass the capacity check in Python. A conditional filter(seats_taken__lt=F("capacity")).update(seats_taken=F("seats_taken") + 1) moves check and increment into one UPDATE; a return of 0 means full.

open as a page

In Django's ORM, what SQL do the field lookups __iexact, __icontains, __in, __range and __isnull produce when searching parking permits?

level: juniorimportance: should knowfreq 55%

basics

~10 s

A lookup is field__operator=value. __iexact and __icontains compare case-insensitively (UPPER ... LIKE on PostgreSQL), __in becomes IN (...), __range becomes an inclusive BETWEEN, and __isnull=True becomes IS NULL; exact=None also becomes IS NULL.

open as a page

In Django, what is a model Manager such as Episode.objects, and what changes when you declare a manager under another name?

level: juniorimportance: should knowfreq 52%

basics

~20 s

A Manager is the class-level interface Django attaches to a model for database queries; its methods return QuerySets. Django adds objects only when a model declares no manager, so naming one catalog removes objects entirely.

open as a page

In Django's ORM, what does Model.objects.create() do, and how does it differ from building an instance and calling save()?

level: juniorimportance: should knowfreq 52%

basics

~20 s

Model.objects.create(**kwargs) builds the instance and saves it with force_insert=True in one call, returning the saved object. It always issues an INSERT, while save() on an instance with a hand-set primary key may issue an UPDATE instead.

open as a page

In Django's ORM, when do you use an aggregate's filter= argument instead of QuerySet.filter(), and where do Case, When and Coalesce fit in?

level: middleimportance: should knowfreq 45%

basics

~20 s

Use filter= when one query needs several aggregates with different conditions, or must keep rows with no matches; QuerySet.filter() narrows all rows for every aggregate. Case/When computes conditional values; Coalesce or default= turns NULL results into zero.

open as a page

In Django's ORM, how does calling values() before annotate() produce a GROUP BY, and what can silently change the grouping?

level: middleimportance: should knowfreq 58%

basics

~20 s

values("field") placed before annotate() makes Django GROUP BY those fields, giving one dict per group with the aggregate added. Putting values() after annotate() keeps per-object grouping, and extra order_by() fields silently join the GROUP BY.

open as a page

In Django, when should you call exists() or count() on a QuerySet instead of using bool() or len()?

level: middleimportance: should knowfreq 55%

basics

~20 s

Use exists() and count() when you need only a yes/no or a number: they send LIMIT 1 and COUNT(*) queries without loading rows. Use bool() and len() when the rows will be used anyway, since they fill the result cache.

open as a page

In Django's ORM, how does slicing a QuerySet become SQL, and which slices or indexes run a query immediately?

level: middleimportance: should knowfreq 45%

basics

~20 s

On an unevaluated QuerySet, qs[a:b] becomes LIMIT and OFFSET and stays lazy. A step slice runs the query and returns a list; qs[n] runs LIMIT 1 at once, raising IndexError if empty. Negative indexes raise ValueError.

open as a page

In Django's ORM, how do you use F() in filter() to compare two columns of the same row, such as permits over their vehicle limit?

level: middleimportance: should knowfreq 50%

basics

~20 s

F('field') refers to a column inside the query instead of a Python value, so filter(vehicles_registered__gt=F('vehicles_allowed')) compares two columns of each row in SQL. F() supports arithmetic and relation paths, such as F('valid_from') + timedelta(days=365) or F('permit__valid_from').

open as a page

With Django's JSONField, how do you filter on a nested key, and how do a missing key, JSON null and SQL NULL differ in 6.1?

level: middleimportance: should knowfreq 45%

basics

~10 s

Chain keys with double underscores: filter(data__owner__name="Bob"), integers index arrays. data__key__isnull=True finds a missing key; data=JSONNull() (6.1) matches top-level JSON null, data__isnull=True matches SQL NULL.

open as a page

In Django's ORM, how do you add a raw SQL fragment to an otherwise normal QuerySet, and why prefer RawSQL over extra()?

level: middleimportance: should knowfreq 38%

basics

~20 s

Wrap the fragment in RawSQL(sql, params, output_field) and use it like any expression in annotate(), filter(pk__in=...) or order_by(). extra() is the older hook that splices strings into clauses; the docs call it a last resort.

open as a page

In Django's ORM, which write calls bypass a model's save() method and its pre_save and post_save signals, and what does QuerySet.delete() still run?

level: middleimportance: should knowfreq 55%

basics

~10 s

QuerySet.update(), bulk_create() and bulk_update() skip save() and the pre_save/post_save signals. QuerySet.delete() skips each instance's delete() method but still sends pre_delete and post_delete for every row it deletes, including Python-emulated cascades.

open as a page

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?

level: seniorimportance: should knowfreq 38%

basics

~10 s

Build 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.

open as a page

Why can a Django QuerySet stored at module level, on a class, or pickled into a cache serve stale rows, and how do you avoid it?

level: seniorimportance: should knowfreq 32%

basics

~20 s

A QuerySet's result cache lives as long as the object. One stored at module or class level fills once and replays those rows for the process's life; pickling evaluates it and freezes the rows. Clone per use, or pickle qs.query.

open as a page

In Django, why does Permit.objects.exclude(violations__paid=False, violations__issued_on__year=2026) not mean 'no unpaid 2026 violation', and what is the fix?

level: seniorimportance: should knowfreq 40%

basics

~20 s

On multi-valued relations, conditions in one exclude() need not refer to the same related row: Django excludes permits with any unpaid violation and any 2026 violation. To exclude permits with a single unpaid 2026 violation, use exclude(violations__in=Violation.objects.filter(paid=False, issued_on__year=2026)).

open as a page

In Django, what breaks when a manager that hides draft episodes is declared first on the model, and how do default_manager_name and base_manager_name help?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The first declared manager becomes the default manager, used by the admin, dumpdata, form choices, unique checks and reverse related managers, so drafts vanish from all of them. Declare a plain manager first or set Meta.default_manager_name.

open as a page

With Django's ORM, how do you expose a database function it lacks by subclassing Func, and what pitfalls come with doing it?

level: seniorimportance: should knowfreq 32%

basics

~10 s

Subclass django.db.models.Func, set function (and optionally template, arity, output_field), and use it in annotate(), filter() or order_by(). Pitfalls: string arguments mean columns, keyword extras are pasted into SQL unescaped, and mixed types need output_field.

open as a page

When importing 200,000 attendee rows with Django's bulk_create() and later correcting them with bulk_update(), which caveats and batch_size choices matter?

level: seniorimportance: should knowfreq 42%

basics

~20 s

bulk_create() inserts in batches without save() or save signals, sets primary keys only on PostgreSQL, MariaDB and SQLite, and loses them with ignore_conflicts. bulk_update() writes CASE WHEN updates per batch; batch_size bounds statement size and memory.

open as a page

Django's ORM has no CTE API; how would you load every employee under a given manager with a recursive CTE and still get Employee instances?

level: seniorimportance: nice to knowfreq 22%

basics

~10 s

Write the WITH RECURSIVE query and pass it to Employee.objects.raw() with the manager id in params, selecting the primary key; or wrap it in RawSQL inside filter(pk__in=...) to keep a chainable QuerySet.

open as a page