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
- 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
queryset = Model.objects.filter(a=1).exclude(b=2) # built, not yet run
list(queryset) # runs it, caches the resultA 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.