Filter concepts by levelShowing all levels.

Django · Django ORM Fundamentals

Django ORM Fundamentals

Laziness, chaining, and caching, then the ten read and five write QuerySet methods

  • QuerySets are lazyno SQL until evaluation — iteration, list(), get(), count()...
  • Chaining clones, never mutatesq1.filter(x) never changes what q1 itself returns
  • get() raises for 0 or >1 matchesfirst()/last() return None instead — pick deliberately
  • update()/delete() skip save()no signals, no auto_now, unless set explicitly

1.QuerySets: laziness, chaining, cloning, and caching

A QuerySet describes a query without running it — no SQL executes until something actually needs the results (iteration, list(), a print, or a method like get()/count()/exists()). Every filter()/exclude() call returns a brand-new, independent QuerySet (chaining, via cloning) rather than mutating the original. Once a QuerySet IS evaluated, its results are cached on that instance — re-iterating the same QuerySet object doesn't re-hit the database, but a fresh slice/index by itself does.

Key features

  • A QuerySet executes SQL only when evaluated — iteration, list(qs), bool(qs), repr(qs)/printing, or a method that returns something other than a QuerySet (get(), count(), exists(), first())
  • filter()/exclude()/order_by() each return a NEW QuerySet (a clone) — the original QuerySet is never mutated, so q1.filter(...) does not change what q1 itself would return
  • Because chaining clones rather than mutates, q2 = q1.exclude(x); q3 = q1.filter(y) — q2 and q3 are independent, both still built from q1's original filters, not from each other
  • Once fully evaluated (e.g. by iterating it completely), a QuerySet caches its results — re-iterating that SAME QuerySet object reuses the cache, no new query
  • Slicing/indexing a QuerySet (queryset[5]) does NOT populate or use the cache by itself — each such access re-queries, unless the whole QuerySet was already evaluated first

What triggers a QuerySet to actually run its SQL

What triggers a QuerySet to actually run its SQL
TriggerExample
Iterationfor entry in queryset: ...
Conversion to a list/boollist(queryset), bool(queryset)
Printing/reprprint(queryset)
A method returning a non-QuerySet value.get(), .count(), .exists(), .first()
NOT a trigger by itselfbuilding the QuerySet: Entry.objects.filter(...) alone runs nothing
Each chained call clones — the original is never mutated
clone +exclude()evaluatedindependentlyevaluatedindependently

q1 = Entry.objects.filter(headline=...)

a QuerySet, not yet run

q2 = q1.exclude(pub_date__gte=today)

a NEW, cloned QuerySet

list(q1)

still just the original filter — unaffected

list(q2)

the filter, minus the excluded rows

  • q1 = Entry.objects.filter(headline=...) — a QuerySet, not yet run
    • leads to q2 = q1.exclude(pub_date__gte=today) (clone + exclude())
    • leads to list(q1) (evaluated independently)
  • q2 = q1.exclude(pub_date__gte=today) — a NEW, cloned QuerySet
    • leads to list(q2) (evaluated independently)
  • list(q1) — still just the original filter — unaffected
  • list(q2) — the filter, minus the excluded rows

Command

python
queryset = Model.objects.filter(a=1).exclude(b=2)   # built, not yet run
list(queryset)                                       # runs it, caches the result

A QuerySet builds a query without running it; filter()/exclude() clone rather than mutate, so a base QuerySet can be safely reused for multiple independent refinements; evaluation (iteration, list(), count(), etc.) is what actually runs the SQL and populates the per-QuerySet cache — slicing/indexing alone bypasses that cache.

2.The retrieval methods

all()/filter()/exclude() return QuerySets (chainable, lazy). get() returns exactly one object or raises (DoesNotExist / MultipleObjectsReturned) — never use it for a "might not exist" lookup. first()/last() return one object or None, no exception. earliest()/latest() need an ordering field. exists()/count() are the cheap way to check presence/size without loading actual rows.

Key features

  • get() raises Model.DoesNotExist for zero matches, Model.MultipleObjectsReturned for more than one — both subclass ObjectDoesNotExist/Exception respectively, and both are real exceptions to catch, not falsy results
  • first()/last() return None (never raise) if there are no matches — the safe alternative to get() when "might not exist" is a normal, expected case
  • first()/last() add an implicit order_by(pk) if the QuerySet has no explicit ordering, to make the result deterministic
  • earliest(*fields)/latest(*fields) require an ordering field (from Meta.get_latest_by or passed explicitly) — they operate on date/datetime-like fields specifically
  • exists() and count() both trigger a real query but avoid loading full row data — exists() is a SELECT 1 LIMIT 1-style check, count() delegates to SQL COUNT()

The retrieval methods, by what they return

The retrieval methods, by what they return
MethodReturnsOn zero matches
all() / filter() / exclude()a QuerySet (still lazy)an empty QuerySet — no error
get()a single model instanceraises DoesNotExist
first() / last()a single instance or Nonereturns None
earliest() / latest()a single instanceraises DoesNotExist
exists()True / FalseFalse
count()an integer0
What each method returns on zero matches
all()/filter()/exclude()
empty QuerySet — no error
get()
raises DoesNotExist
earliest()/latest()
raises DoesNotExist
first()/last()
returns None
exists()/count()
False / 0
  • all()/filter()/exclude(): returns a QuerySet, safe on zero — empty QuerySet — no error
  • get(): returns a value, raises on zero — raises DoesNotExist
  • earliest()/latest(): returns a value, raises on zero — raises DoesNotExist
  • first()/last(): returns a value, safe on zero — returns None
  • exists()/count(): returns a value, safe on zero — False / 0

Command

python
if Order.objects.filter(customer=customer).exists():   # cheap presence check
    ...
count = Order.objects.filter(status="PENDING").count()  # cheap size check

all()/filter()/exclude() stay lazy and chainable; get() demands exactly one match and raises otherwise (use it only where >1 would be a real bug); first()/last() return None instead of raising; exists()/count() are the cheap presence/size checks — exists() for yes/no, count() only when the actual number matters.

3.The write methods

update() writes new values to every row a QuerySet currently matches, in one SQL UPDATE — it does NOT call save() or send signals. delete() removes every matched row (plus CASCADEs). create() is get()-and-save() combined into one call. get_or_create()/update_or_create() both return a (object, created) tuple, and both use a transaction internally to reduce (not eliminate) race conditions between two concurrent callers.

Key features

  • update(**kwargs) returns the number of rows affected — it bypasses save(), so pre_save/post_save signals and auto_now fields do NOT fire
  • delete() (called on a QuerySet) returns (total_deleted, {model_label: count}) — same shape as Model.delete()
  • create(**kwargs) is exactly Model(**kwargs) followed by .save() — one call, but still runs full save() behavior (signals, auto_now, etc.)
  • get_or_create(defaults=None, **kwargs) returns (object, created: bool) — defaults supplies fields used ONLY on creation, not for the lookup itself
  • get_or_create()/update_or_create() wrap the get-then-create logic in a transaction to reduce race conditions, but a genuine guarantee against duplicates still requires a database-level unique constraint

The write methods

The write methods
MethodRuns save()/signals?Returns
update()no — direct SQL UPDATEnumber of rows affected
delete() (QuerySet)no — direct SQL DELETE (+ CASCADE)(total_deleted, {label: count})
create()yesthe new instance
get_or_create()yes, if created(instance, created: bool)
update_or_create()yes, always(instance, created: bool)
Which write methods actually run save()
update()
direct SQL UPDATE — no auto_now
delete() (QuerySet)
direct SQL DELETE + CASCADE
create()
Model(**kwargs) + save()
get_or_create() / update_or_create()
save() runs on create/update
  • update(): bypasses save()/signals, bulk (matched rows) — direct SQL UPDATE — no auto_now
  • delete() (QuerySet): bypasses save()/signals, bulk (matched rows) — direct SQL DELETE + CASCADE
  • create(): runs save()/signals, single instance — Model(**kwargs) + save()
  • get_or_create() / update_or_create(): runs save()/signals, single instance — save() runs on create/update

Command

python
obj, created = Model.objects.get_or_create(lookup_field=value, defaults={"other_field": x})
obj, created = Model.objects.update_or_create(lookup_field=value, defaults={"other_field": x})

update()/delete() are direct SQL against the QuerySet's matched rows — no save(), no signals, no auto_now, unless set explicitly. create()/get_or_create()/update_or_create() always go through save(). Only keyword args OUTSIDE defaults are used for the get_or_create()/update_or_create() lookup.

Remember

  • A QuerySet builds a query without running it; evaluation (iteration, list(), count(), etc.) is what actually runs the SQL.
  • get() demands exactly one match and raises otherwise — use first()/last() when zero-or-more is a normal outcome.
  • exists() for a yes/no presence check; count() only when the actual number matters — count() > 0 wastes a full tally.
  • update()/delete() are direct SQL against matched rows — no save(), no signals, no auto_now unless listed explicitly.

Pro tip

  • Only get_or_create()/update_or_create() kwargs OUTSIDE defaults are used for the lookup — fields meant only for creation belong inside defaults.
  • A QuerySet's results cache only populates on full evaluation — slicing/indexing alone re-queries every time.

Next up

  • Query Expressions
  • Aggregation and Annotation
  • Advanced ORM Queries

Sources

  • Making queries — docs.djangoproject.com/en/5.2/topics/db/queries/
  • QuerySet API reference — docs.djangoproject.com/en/5.2/ref/models/querysets/
  • Every claim checked against Django 5.2 LTS docs · checked 2026-08-24

Spotted a mistake on this page? Tell us.