skip to content

Fetch Modes & Deferred Columns

Fetch modes (6.1) decide what an unloaded field access does: FETCH_ONE queries per instance, FETCH_PEERS loads the batch, FETCH_RAISE blocks it; only() and defer() trim columns.

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

explore

questions

4

In Django's ORM, what do QuerySet.only() and defer() do, and what does reading a deferred field cost you later?

level: middleimportance: must knowfreq 55%

answer

  1. still model instances, fewer columns
  2. one replaces, one adds
  3. primary key is never deferred
  4. a query per instance per field

basics

~20 s

defer() leaves named columns out of the SELECT and only() loads just the named ones; you still get model instances. Reading a skipped field later runs one extra query for that instance under the default FETCH_ONE mode.

solid answer

~40 s

`defer("description")` drops columns from the `SELECT`; `only("id", "title", "status")` keeps just those. Both return normal model instances, which is the point compared with `values()`. The primary key is always loaded. `defer()` calls **add** to the deferred set and `defer(None)` clears it; each `only()` call **replaces** the loaded set. The cost comes later: touching a deferred field calls `refresh_from_db(fields=[...])` for that one instance, so a loop that reads `ticket.description` on 50 tickets adds 50 queries under the default `FETCH_ONE` mode (Django 6.1's `FETCH_PEERS` batches them into one query per field). Other edges: `save()` on such an instance writes only the loaded fields, deferred fields raise `SynchronousOnlyOperation` in async code instead of loading, and combining `only()` with `select_related()` must keep the foreign key or Django raises a `FieldError`.

code

python · 12 lines
python
from helpdesk.models import Ticket

tickets = Ticket.objects.only("id", "title", "status")  # description not selected

for t in tickets:
    print(t.title)          # loaded, no query
    print(t.description)    # deferred: one extra SELECT per ticket (FETCH_ONE)

t = tickets[0]
t.get_deferred_fields()     # {'description', 'assignee_id', ...}
t.status = "closed"
t.save()                    # UPDATE writes only the loaded fields

go deeper

for a junior

Recall that only() and defer() still return model instances with fewer columns loaded, and that the primary key is always included.

for a middle

Explain additive defer() versus replacing only(), the per-instance refresh a deferred read costs, and the save() and select_related() rules.

for a senior

Show when trimming columns pays off, how a template or serializer reading an extra field silently undoes it, and what to use instead.

for a principal

Weigh ad hoc only() calls against modelling fixes such as splitting wide columns into their own table or an unmanaged read model.

## What the two methods change Every Django `QuerySet` for a model normally selects **all concrete columns** of that model. Two methods trim that list while still returning **model instances**: - **`defer(*fields)`** — load everything *except* the named fields. `Ticket.objects.defer("description", "attachments_json")`. - **`only(*fields)`** — load *only* the named fields (plus the primary key). `Ticket.objects.only("title", "status", "assignee")`. Both are useful when a model has a wide column — a long `TextField`, a large `JSONField`, a binary blob — that a list page never shows. ## How calls combine | Call | Effect | |---|---| | `defer("a").defer("b")` | both `a` and `b` deferred — `defer()` is **additive** | | `only("a", "b").only("c")` | only `c` loaded — each `only()` **replaces** the set | | `only("a", "b").defer("b")` | only `a` loaded | | `defer(None)` | clears the deferred set; everything loads | | `only()` with no arguments | loads every field, annotations included | Rules that always hold: 1. **The primary key is never deferred.** Django needs it to identify the instance and to fetch the rest later. 2. **Annotations are always fetched.** Naming an aggregate in `only()` or `defer()` raises an error. 3. **`select_related()` needs the link.** `Ticket.objects.select_related("assignee").only("title")` raises `FieldError: Field Ticket.assignee cannot be both deferred and traversed using select_related at the same time.` Include it — `only("title", "assignee__username")` loads the ticket's title, the foreign key and the joined user's username. ## The deferred-field cost A deferred field is not missing; it is **loaded on demand**. When code reads `ticket.description` and the value is not in the instance's `__dict__`, Django's field descriptor asks the instance's **fetch mode** what to do: - **`FETCH_ONE`** (the default) calls `refresh_from_db(fields=["description"])` — **one `SELECT` for that instance and that field**. Loop over 50 tickets and read two deferred fields, and you have added 100 queries. - **`FETCH_PEERS`** (6.1) loads that field for every instance from the same QuerySet in **one query per field**. - **`FETCH_RAISE`** (6.1) raises `FieldFetchBlocked` instead of querying. `instance.get_deferred_fields()` returns the set of attribute names not yet loaded, which is handy in tests and debugging. ## Side effects beyond reading - **Saving.** Calling `save()` on an instance with deferred fields automatically becomes an `update_fields` save of the **loaded** fields only, so a deferred column is not overwritten with a stale value. - **Async code.** Deferred fields do not lazy-load from asynchronous code; you get `SynchronousOnlyOperation`. List every field an async path touches in `only()`. - **Unsaved instances.** Reading a deferred field on an instance without a primary key raises `AttributeError`, because there is nothing to fetch by. ## Diagnosing a deferred-field N+1 The symptom looks like any per-row query storm, but the SQL gives it away: many near-identical `SELECT "helpdesk_ticket"."id", "helpdesk_ticket"."description" FROM "helpdesk_ticket" WHERE "helpdesk_ticket"."id" = %s` statements, each re-reading **one column of the same table** by primary key. Steps that find the cause quickly: 1. Find the QuerySet that built the instances and look for `only()` or `defer()` — often added in a manager or a shared helper, far from the template. 2. List what the consumer (template, serializer, export) actually reads, and compare it with the loaded fields. 3. Fix by adding the field to `only()`, removing the trim, or — on 6.1 — using `FETCH_PEERS` so the leftovers cost one query per field instead of one per row. 4. Re-count the queries; a deferred-field fix should leave exactly one `SELECT` for the table. Because the extra queries hit the **same table** by primary key, they are easy to mistake for harmless lookups; on a list of hundreds of rows they are not. ## When to use it — and when not The documentation is blunt: these methods are for **advanced use**, after measuring that the skipped columns matter. Guidance worth repeating in an interview: - Use `defer()` when you **cannot know at query time** whether a wide field will be needed, and it usually is not. - If a page always needs the same small subset, `only()` is fine, but consider whether you need instances at all — `values()`/`values_list()` skip instance construction entirely. - If a subset is used everywhere, the docs suggest **moving the wide data to its own model**, or an unmanaged model (`Meta.managed = False`) over the same table with just the common fields. - Never add `only()` to a queryset and then pass it to code — a template, a serializer — that reads other fields. That turns one query into N+1 quietly.

  • What does save() write for an instance loaded with only("title", "status")?
    Django notices the instance has deferred fields and turns the save into an `update_fields` save of the loaded fields only — here `title` and `status`. Deferred columns are left untouched in the database, so a partial load cannot overwrite them with missing values.
  • How do only() and select_related() work together?
    They combine, but the foreign key being joined must stay loaded. Use the double-underscore form to trim the related model too: `select_related("assignee").only("title", "assignee__username")`. Leaving the key out raises `FieldError` saying the field cannot be both deferred and traversed.
  • What happens when async Django code reads a field excluded by only()?
    Deferred fields do not lazy-load from asynchronous code; the access raises `SynchronousOnlyOperation` instead of querying. Async paths must name every field they read in `only()`, or avoid trimming columns for that QuerySet.

saying these in an interview costs you the question

  • Says only() returns dictionaries rather than model instances
  • Believes a second only() call adds to the first
  • Thinks reading a deferred field raises an error by default
  • Claims the primary key can be deferred
  • Assumes save() on a partially loaded instance writes every column
open as a page

In Django 6.1, what does QuerySet.fetch_mode() control, and how do FETCH_ONE, FETCH_PEERS and FETCH_RAISE differ?

level: juniorimportance: should knowfreq 35%

basics

~20 s

fetch_mode() decides what happens when code reads a field the query did not load: FETCH_ONE (default) queries for that instance, FETCH_PEERS loads it for every instance from the same QuerySet at once, FETCH_RAISE raises FieldFetchBlocked.

open as a page

A Django 6.1 ticket-list API runs 101 queries because each row shows its assignee; when is FETCH_PEERS the right fix, and when is explicit loading better?

level: seniorimportance: should knowfreq 30%

basics

~10 s

FETCH_PEERS turns 101 queries into 2 without naming relations, suiting code whose field access varies. Explicit loading wins when needs are known: select_related is one JOIN, and only prefetch_related covers reverse and many-to-many sets.

open as a page

How can Django 6.1's FETCH_RAISE fetch mode guard a performance-critical view against accidental lazy loads, and what will it not catch?

level: seniorimportance: nice to knowfreq 20%

basics

~10 s

Build the view's QuerySet with fetch_mode(models.FETCH_RAISE) plus the select_related/only plan it needs; any unplanned foreign key or deferred-field access raises FieldFetchBlocked. It does not catch related-manager queries or queries the code writes explicitly.

open as a page