Chapter 9 · QuerySets

9.6 Evaluation and the result cache

A queryset is evaluated the first time it is iterated or asked its length or its truth: QuerySet._fetch_all sends its statement and keeps the list of what came back as QuerySet._result_cache. Later uses read the list; a narrower question put to a queryset that has no list, an index, QuerySet.count, QuerySet.exists, is answered without evaluating it; and QuerySet.iterator reads rows without keeping them.

Two costs pull against each other here. A queryset that is used twice, tested for emptiness and then looped over, should not send its statement twice, so the rows have to be kept. But keeping them means that every row of the result is in memory as an instance for as long as the queryset is, and a question such as how many rows there are should not need that. Django’s answer has three parts. An evaluation is whole: the first use that needs the rows fetches all of them and makes the list. A narrower question put to a queryset that has not been evaluated is answered by a narrower statement, and leaves the queryset as it was. And for a result too large to keep there is a method that reads it once and keeps nothing.

_fetch_all

QuerySet._fetch_all does two things. Where there is no result cache it makes an instance of the queryset’s iterable class, handing it the queryset, and stores the list of everything that instance yields. Then, where the queryset has prefetch lookups and its QuerySet._prefetch_done is not yet true, it follows them, which sets it. The Python protocols of the class call it: QuerySet.__iter__ calls it and returns an iterator over the list, and QuerySet.__len__ and QuerySet.__bool__ call it and return the list’s length and its truth.

The recordings on this page were made from a running Django, on SQLite, over the rows that the chapter’s opening page, QuerySets, lists: the lines set in are calls, nested as they were made, -> is what a call or an expression returned, and a line that begins SQL is a statement as Django handed it to its cursor. The queryset called named is the crew of the Petrel in order of name.

for sailor in named: seen(sailor)
  QuerySet.__iter__
    QuerySet._fetch_all  (there is no result cache)
      ModelIterable.__iter__
        Query.get_compiler(using='default')
        SQLCompiler.execute_sql
          SQL SELECT "crew_sailor"."id", "crew_sailor"."name", "crew_sailor"."ship_id", "crew_sailor"."signed_on" FROM "crew_sailor" WHERE "crew_sailor"."ship_id" = %s ORDER BY "crew_sailor"."name" ASC with params (1,)
          the cursor's fetchmany(100)  ->  2 rows
          the cursor's fetchmany(100)  ->  0 rows
        SQLCompiler.results_iter
        Model.from_db('default', ['id', 'name', 'ship_id', 'signed_on'], [1, 'Ada', 1, datetime(2011, 6, 1, 8, 0, tzinfo=UTC)], fetch_mode=FETCH_ONE)  (Sailor)
        Model.from_db('default', ['id', 'name', 'ship_id', 'signed_on'], [2, 'Bram', 1, datetime(2026, 4, 10, 9, 0, tzinfo=UTC)], fetch_mode=FETCH_ONE)  (Sailor)
  the loop's body runs with <Sailor: Ada>
  the loop's body runs with <Sailor: Bram>

The iterable is where the query is run, and how it turns a row into an instance is told in From rows to instances: ModelIterable and select_related. What matters here is the order. SQLCompiler.execute_sql sent the statement and then read every row from the cursor, a hundred at a call until a call returned none, before it returned; both instances were made; and only then did the loop’s body run for the first time. The hundred is GET_ITERATOR_CHUNK_SIZE, the number of rows asked of the cursor at once, and has no bearing on how many are fetched in all.

What the cache answers, and what a queryset answers without one

With a result cache, these uses of a queryset read the list:

len(named)
  QuerySet.__len__
    QuerySet._fetch_all  (the result cache is filled)
    -> 2
bool(named)
  QuerySet.__bool__
    QuerySet._fetch_all  (the result cache is filled)
    -> True
named[0]
  QuerySet.__getitem__(0)  ->  <Sailor: Ada>
named.count()
  QuerySet.count  ->  2
named.exists()
  QuerySet.exists  ->  True
named.contains(dag)
  QuerySet.contains(<Sailor: Dag>)  ->  False
for hand in named: seen(hand)
  QuerySet.__iter__
    QuerySet._fetch_all  (the result cache is filled)
  the loop's body runs with <Sailor: Ada>
  the loop's body runs with <Sailor: Bram>

An index is an index into the list, and QuerySet.count, QuerySet.exists and QuerySet.contains are its length, its truth and a membership test. No statement was sent for any of them.

Without a result cache the same uses divide. Iteration, len and bool evaluate the queryset, as above. The others are each answered without evaluating the queryset, on a clone or on a copy of the query. Here fresh is another queryset of the same crew:

fresh.count()
  QuerySet.count
    Query.get_count('default')
      Query.get_compiler(using='default')
      SQLCompiler.execute_sql(result_type='single')
        SQL SELECT COUNT(*) AS "__count" FROM "crew_sailor" WHERE "crew_sailor"."ship_id" = %s with params (1,)
        the cursor's fetchone()  ->  a row
      -> 2
    -> 2
fresh.exists()
  QuerySet.exists
    Query.has_results('default')
      Query.set_limits(high=1)
      Query.get_compiler(using='default')
      SQLCompiler.execute_sql(result_type='single')
        SQL SELECT %s AS "a" FROM "crew_sailor" WHERE "crew_sailor"."ship_id" = %s LIMIT 1 with params (1, 1)
        the cursor's fetchone()  ->  a row
      -> True
    -> True
fresh.contains(dag)
  QuerySet.contains(<Sailor: Dag>)
    QuerySet.exists
      Query.has_results('default')
        Query.set_limits(high=1)
        Query.get_compiler(using='default')
        SQLCompiler.execute_sql(result_type='single')
          SQL SELECT %s AS "a" FROM "crew_sailor" WHERE ("crew_sailor"."ship_id" = %s AND "crew_sailor"."id" = %s) LIMIT 1 with params (1, 1, 4)
          the cursor's fetchone()  ->  no row
        -> False
      -> False
    -> False
fresh[0]
  QuerySet.__getitem__(0)
    Query.set_limits(low=0, high=1)
    QuerySet._fetch_all  (there is no result cache)
      ModelIterable.__iter__
        Query.get_compiler(using='default')
        SQLCompiler.execute_sql
          SQL SELECT "crew_sailor"."id", "crew_sailor"."name", "crew_sailor"."ship_id", "crew_sailor"."signed_on" FROM "crew_sailor" WHERE "crew_sailor"."ship_id" = %s ORDER BY "crew_sailor"."name" ASC LIMIT 1 with params (1,)
          the cursor's fetchmany(100)  ->  1 row
          the cursor's fetchmany(100)  ->  0 rows
        SQLCompiler.results_iter
        Model.from_db('default', ['id', 'name', 'ship_id', 'signed_on'], [1, 'Ada', 1, datetime(2011, 6, 1, 8, 0, tzinfo=UTC)], fetch_mode=FETCH_ONE)  (Sailor)
    -> <Sailor: Ada>
fresh[1:3]
  QuerySet.__getitem__(slice(1, 3, None))
    Query.set_limits(low=1, high=3)
    -> a SailorQuerySet of Sailor, no result cache
fresh[0:4:2]
  QuerySet.__getitem__(slice(0, 4, 2))
    Query.set_limits(low=0, high=4)
    QuerySet.__iter__
      QuerySet._fetch_all  (there is no result cache)
        ModelIterable.__iter__
          Query.get_compiler(using='default')
          SQLCompiler.execute_sql
            SQL SELECT "crew_sailor"."id", "crew_sailor"."name", "crew_sailor"."ship_id", "crew_sailor"."signed_on" FROM "crew_sailor" WHERE "crew_sailor"."ship_id" = %s ORDER BY "crew_sailor"."name" ASC LIMIT 4 with params (1,)
            the cursor's fetchmany(100)  ->  2 rows
            the cursor's fetchmany(100)  ->  0 rows
          SQLCompiler.results_iter
          Model.from_db('default', ['id', 'name', 'ship_id', 'signed_on'], [1, 'Ada', 1, datetime(2011, 6, 1, 8, 0, tzinfo=UTC)], fetch_mode=FETCH_ONE)  (Sailor)
          Model.from_db('default', ['id', 'name', 'ship_id', 'signed_on'], [2, 'Bram', 1, datetime(2026, 4, 10, 9, 0, tzinfo=UTC)], fetch_mode=FETCH_ONE)  (Sailor)
    QuerySet.__len__
      QuerySet._fetch_all  (the result cache is filled)
      -> 2
    -> [<Sailor: Ada>]
fresh._result_cache  ->  None

count hands the query to Query.get_count, which asks the database to count. exists hands it to Query.has_results, which has Query.exists make a copy of the query that selects a constant and stops at one row; contains is exists on a clone filtered by the object’s primary key. An index makes a clone limited to the one row and evaluates the clone. The plain slice sent nothing. Under the slice with a step, the QuerySet.__len__ that follows QuerySet.__iter__ is Python’s doing: QuerySet.__getitem__ makes a list of the clone, and list asks its argument for a length once it has taken an iterator from it. The last line shows that after all six, fresh still has no list: a program that asks an unevaluated queryset for its count and then loops over it has sent two statements.

Table 9.6 How each use of a queryset is answered. The methods that return one object or one value, QuerySet.get and QuerySet.first among them, are in get, first, count and the other methods that run a query at once.
use with a result cache without one
iteration, len, bool with a result cache reads the list without one evaluates the queryset: _fetch_all fills the cache
pickling with a result cache pickles the list with the rest without one evaluates the queryset first
an index, [k] with a result cache the item of the list without one a clone limited to that row is evaluated; the queryset is not
a slice, [a:b] with a result cache a slice of the list, which is a list without one a clone with the limits on its query, not evaluated
a slice with a step, [a:b:c] with a result cache a slice of the list without one a clone with the limits is evaluated, and the step is taken from its list
repr with a result cache the first twenty of the list, and a note where there are more without one a clone limited to twenty-one rows is evaluated
count with a result cache the list’s length without one Query.get_count: a statement that counts
exists with a result cache the list’s truth without one Query.has_results: a statement that stops at one row
contains with a result cache whether the object is in the list without one exists on a clone filtered by the object’s primary key
a shallow copy, copy.copy with a result cache the copy shares the list without one evaluates the queryset first, as pickling does
iterator with a result cache does not look at it without one runs the statement, and keeps nothing

An index and a slice

QuerySet.__getitem__ takes an integer or a slice and refuses anything else with TypeError, and refuses a negative index or bound with ValueError. With a result cache it indexes the list. Without one it makes a clone and narrows the clone’s query with Query.set_limits. An index past the last row raises IndexError either way. Without a result cache that is because the clone is evaluated, its list is empty, and the first item is asked of it.

A query keeps its limits as two marks, Query.low_mark and Query.high_mark, the offsets of the first row wanted and of the first row not wanted, and set_limits applies new bounds within the old ones. So a slice of a slice counts from the first slice’s start and cannot reach past its end. Below, everyone is the queryset of all four sailors by key.

everyone[1:3].query.low_mark, everyone[1:3].query.high_mark  ->  (1, 3)
everyone[5:].query.low_mark, everyone[5:].query.high_mark, everyone[5:][2:4].query.low_mark, everyone[5:][2:4].query.high_mark  ->  (5, None, 7, 9)
everyone[:3][:10].query.high_mark, everyone[2:6][1:].query.low_mark  ->  (3, 3)
type(everyone[1:3]).__name__, type(everyone[0:4:2]).__name__  ->  ('SailorQuerySet', 'list')
  SQL SELECT "crew_sailor"."id", "crew_sailor"."name", "crew_sailor"."ship_id", "crew_sailor"."signed_on" FROM "crew_sailor" ORDER BY "crew_sailor"."id" ASC LIMIT 4
everyone[-1]  ->  raises ValueError: Negative indexing is not supported.
everyone[:-1]  ->  raises ValueError: Negative indexing is not supported.
everyone['id']  ->  raises TypeError: QuerySet indices must be integers or slices, not str.
everyone[9]  ->  raises IndexError: list index out of range
  SQL SELECT "crew_sailor"."id", "crew_sailor"."name", "crew_sailor"."ship_id", "crew_sailor"."signed_on" FROM "crew_sailor" ORDER BY "crew_sailor"."id" ASC LIMIT 1 OFFSET 9
isinstance(everyone[2:2], EmptyQuerySet), list(everyone[2:2])  ->  (True, [])

A plain slice returns the clone, still unevaluated, so it can be passed on like any queryset. A slice with a step is the one slice that evaluates: the clone is limited to the bounds, turned into a list, and the step applied to the list. And where the two marks meet, set_limits marks the query as empty, which makes the slice an empty queryset: isinstance with EmptyQuerySet is true of any queryset whose query is so marked, and evaluating it sends no statement (Annotations, ordering and the other methods that change the query has the empty queryset).

Once a query has limits, some of the queryset’s methods refuse it. For a filter and an ordering Django’s documentation gives the reason: that does not translate well into SQL and would not have a clear meaning. Query.is_sliced is true when either mark has moved, and each of these tests it:

everyone[1:3].filter(name='Bram')  ->  raises TypeError: Cannot filter a query once a slice has been taken.
everyone[1:3].order_by('name')  ->  raises TypeError: Cannot reorder a query once a slice has been taken.
everyone[1:3].reverse()  ->  raises TypeError: Cannot reverse a query once a slice has been taken.
everyone[1:3].distinct()  ->  raises TypeError: Cannot create distinct fields once a slice has been taken.
everyone[1:3].update(name='x')  ->  raises TypeError: Cannot update a query once a slice has been taken.
everyone[1:3].delete()  ->  raises TypeError: Cannot use 'limit' or 'offset' with delete().
everyone[1:3].in_bulk()  ->  raises TypeError: Cannot use 'limit' or 'offset' with in_bulk().
everyone[1:3].latest('signed_on')  ->  raises TypeError: Cannot change a query once a slice has been taken.
list(everyone[1:3].values_list('name', flat=True)), everyone[1:3].count(), everyone[1:3].exists()  ->  (['Bram', 'Cora'], 2, True)
  SQL SELECT "crew_sailor"."name" AS "name" FROM "crew_sailor" ORDER BY "crew_sailor"."id" ASC LIMIT 2 OFFSET 1
  SQL SELECT COUNT(*) FROM (SELECT "crew_sailor"."id" AS "col1" FROM "crew_sailor" ORDER BY "crew_sailor"."id" ASC LIMIT 2 OFFSET 1) subquery
  SQL SELECT %s AS "a" FROM "crew_sailor" LIMIT 1 OFFSET 1 with params (1,)

QuerySet.filter and QuerySet.exclude refuse only when they are given an argument. QuerySet.values_list, QuerySet.count and QuerySet.exists in the last question are among the methods that do not refuse a sliced query: the count is taken over the slice as a subquery, and exists asks whether the slice has a row. What the operators that combine two querysets do with a sliced one is in Combining querysets: the operators and union.

iterator: reading without keeping

QuerySet.iterator returns a generator over the same iterable class that QuerySet._fetch_all uses, and stores nothing: the queryset has no result cache afterwards, and one it already had is neither read nor changed. It also asks the compiler for a chunked fetch, unless the database’s entry in DATABASES has "DISABLE_SERVER_SIDE_CURSORS" set. With a chunked fetch SQLCompiler.execute_sql takes its cursor from BaseDatabaseWrapper.chunked_cursor and, where the backend’s feature BaseDatabaseFeatures.can_use_chunked_reads is true, returns without having read the rows: they are read from the cursor as the consumer asks for them, a chunk at a call.

iterator: rows are read from the cursor a chunk at a time, and each becomes an instance as the loop asks for it
    everyone = Sailor.objects.order_by('id')
    for sailor in everyone.iterator(chunk_size=3): seen(sailor)
      QuerySet.iterator(chunk_size=3)  ->  a generator
      QuerySet._iterator(use_chunked_fetch=True, chunk_size=3)
        ModelIterable.__iter__
          SQLCompiler.execute_sql(chunked_fetch=True, chunk_size=3)
            BaseDatabaseWrapper.chunked_cursor
              BaseDatabaseWrapper.cursor
            SQL SELECT "crew_sailor"."id", "crew_sailor"."name", "crew_sailor"."ship_id", "crew_sailor"."signed_on" FROM "crew_sailor" ORDER BY "crew_sailor"."id" ASC
          SQLCompiler.results_iter
          the cursor's fetchmany(3)  ->  3 rows
          Model.from_db('default', ['id', 'name', 'ship_id', 'signed_on'], [1, 'Ada', 1, datetime(2011, 6, 1, 8, 0, tzinfo=UTC)], fetch_mode=FETCH_ONE)  (Sailor)
      the loop's body runs with <Sailor: Ada>
          Model.from_db('default', ['id', 'name', 'ship_id', 'signed_on'], [2, 'Bram', 1, datetime(2026, 4, 10, 9, 0, tzinfo=UTC)], fetch_mode=FETCH_ONE)  (Sailor)
      the loop's body runs with <Sailor: Bram>
          Model.from_db('default', ['id', 'name', 'ship_id', 'signed_on'], [3, 'Cora', 2, datetime(2011, 6, 1, 8, 0, tzinfo=UTC)], fetch_mode=FETCH_ONE)  (Sailor)
      the loop's body runs with <Sailor: Cora>
          the cursor's fetchmany(3)  ->  1 row
          Model.from_db('default', ['id', 'name', 'ship_id', 'signed_on'], [4, 'Dag', 2, datetime(2026, 4, 10, 9, 0, tzinfo=UTC)], fetch_mode=FETCH_ONE)  (Sailor)
      the loop's body runs with <Sailor: Dag>
          the cursor's fetchmany(3)  ->  0 rows
    everyone._result_cache  ->  None

Three rows were read, and the loop’s body ran after each instance was made, before the next was. The fourth row was not read from the cursor until the loop asked for a fourth instance. Compare the same loop over the queryset itself, where everything is read and made first:

The same loop over the queryset itself: every row is read and every instance made before the loop sees the first
    for sailor in everyone: seen(sailor)
      QuerySet.__iter__
        QuerySet._fetch_all
          ModelIterable.__iter__
            SQLCompiler.execute_sql
              BaseDatabaseWrapper.cursor
              SQL SELECT "crew_sailor"."id", "crew_sailor"."name", "crew_sailor"."ship_id", "crew_sailor"."signed_on" FROM "crew_sailor" ORDER BY "crew_sailor"."id" ASC
              the cursor's fetchmany(100)  ->  4 rows
              the cursor's fetchmany(100)  ->  0 rows
            SQLCompiler.results_iter
            Model.from_db('default', ['id', 'name', 'ship_id', 'signed_on'], [1, 'Ada', 1, datetime(2011, 6, 1, 8, 0, tzinfo=UTC)], fetch_mode=FETCH_ONE)  (Sailor)
            Model.from_db('default', ['id', 'name', 'ship_id', 'signed_on'], [2, 'Bram', 1, datetime(2026, 4, 10, 9, 0, tzinfo=UTC)], fetch_mode=FETCH_ONE)  (Sailor)
            Model.from_db('default', ['id', 'name', 'ship_id', 'signed_on'], [3, 'Cora', 2, datetime(2011, 6, 1, 8, 0, tzinfo=UTC)], fetch_mode=FETCH_ONE)  (Sailor)
            Model.from_db('default', ['id', 'name', 'ship_id', 'signed_on'], [4, 'Dag', 2, datetime(2026, 4, 10, 9, 0, tzinfo=UTC)], fetch_mode=FETCH_ONE)  (Sailor)
      the loop's body runs with <Sailor: Ada>
      the loop's body runs with <Sailor: Bram>
      the loop's body runs with <Sailor: Cora>
      the loop's body runs with <Sailor: Dag>
    len(everyone._result_cache)  ->  4

The argument chunk_size= of iterator is the number of rows asked of the cursor at a call, and it is 2,000 when none is given. A number that is not positive is refused with ValueError. Where the queryset has prefetch lookups the argument is required, and is then also the number of instances for which the lookups are followed at a time (prefetch_related: related objects in a second query).

In the base class chunked_cursor returns an ordinary cursor, and its docstring says what a backend’s own may return instead, where the database supports it: a cursor that tries to avoid caching, the server-side cursor that "DISABLE_SERVER_SIDE_CURSORS" is named for. The setting is for a connection that cannot use such a cursor, and Django’s documentation gives the case of a connection pooler in transaction pooling mode. What each backend does is in Cursors: executing a statement, the query log and execute wrappers. On a connection with the setting on, iterator still makes one instance at a time and keeps none, but execute_sql has read every row first, as it does for _fetch_all.

repr, pickling and copying

Printing a queryset, pickling it and copying it are uses of it too:

Asked of repr, pickle and copy
    repr(Port.objects.all())  ->  '<QuerySet [<Port: Bergen>, <Port: Leith>, <Port: Lisbon>]>'
      SQL SELECT "fleet_port"."id", "fleet_port"."name", "fleet_port"."country" FROM "fleet_port" ORDER BY "fleet_port"."name" ASC LIMIT 21
    repr(Logbook.objects.order_by('id')).count('<Logbook'), repr(Logbook.objects.order_by('id'))[-45:]  ->  (20, "0)>, '...(remaining elements truncated)...']>")
      SQL SELECT "office_logbook"."id", "office_logbook"."title" FROM "office_logbook" ORDER BY "office_logbook"."id" ASC LIMIT 21
      SQL SELECT "office_logbook"."id", "office_logbook"."title" FROM "office_logbook" ORDER BY "office_logbook"."id" ASC LIMIT 21
    len(pickle.loads(pickle.dumps(Port.objects.all()))._result_cache)  ->  3
      SQL SELECT "fleet_port"."id", "fleet_port"."name", "fleet_port"."country" FROM "fleet_port" ORDER BY "fleet_port"."name" ASC
...
    ports = Port.objects.all(); list(ports)
      SQL SELECT "fleet_port"."id", "fleet_port"."name", "fleet_port"."country" FROM "fleet_port" ORDER BY "fleet_port"."name" ASC
    len(ports._result_cache), copy.deepcopy(ports)._result_cache, copy.copy(ports)._result_cache is ports._result_cache  ->  (3, None, True)
    unread = Port.objects.all()
    len(copy.copy(unread)._result_cache), len(unread._result_cache)  ->  (3, 3)
      SQL SELECT "fleet_port"."id", "fleet_port"."name", "fleet_port"."country" FROM "fleet_port" ORDER BY "fleet_port"."name" ASC
    Port.objects.all() == Port.objects.all(), ports == ports  ->  (False, True)

QuerySet.__repr__ takes a slice of twenty-one, one more than REPR_OUTPUT_SIZE, and so sends a statement with that limit when the queryset has not been evaluated; a twenty-first row, which the office’s twenty-five logbooks have, is replaced in what it prints by a note that the rest were left out. Printing a queryset in a shell or a debugger is a query.

QuerySet.__getstate__ evaluates the queryset and returns its attributes, the list among them, with the version of Django under the key "_django_version". QuerySet.__setstate__ warns, with RuntimeWarning, when the version it finds is not the one running, or when there is none, and restores the attributes either way. A pickled queryset is therefore a pickled result, and unpickling it sends nothing.

QuerySet.__deepcopy__ copies every attribute deeply but the result cache, which it leaves as None on the copy, so a deep copy of an evaluated queryset is an unevaluated one. The copy has the original’s _prefetch_done with the rest, so where the original has followed its prefetch lookups, evaluating the copy does not follow them. The class defines no __copy__, so a shallow copy is made by Python from what __getstate__ returns: it evaluates a queryset that has not been evaluated, as pickling does, and the copy shares the list. The question put to the queryset called unread shows the statement. The class defines no __eq__, so two querysets are equal only when they are the same object.

A queryset given as a value

A queryset can be the value of another’s condition, and that is not a use of its contents:

A queryset given as a value to another's filter is not evaluated: its query goes into the other's statement
    heavy = Ship.objects.filter(tonnage__gt=1000)
    on_heavy = Sailor.objects.filter(ship__in=heavy)
      QuerySet.resolve_expression
    heavy._result_cache, on_heavy._result_cache  ->  (None, None)
    for sailor in on_heavy: seen(sailor)
      QuerySet.__iter__
        QuerySet._fetch_all  (there is no result cache)
          ModelIterable.__iter__
            Query.get_compiler(using='default')
            SQLCompiler.execute_sql
              SQL SELECT "crew_sailor"."id", "crew_sailor"."name", "crew_sailor"."ship_id", "crew_sailor"."signed_on" FROM "crew_sailor" WHERE "crew_sailor"."ship_id" IN (SELECT "U0"."id" FROM "fleet_ship" "U0" WHERE "U0"."tonnage" > %s) with params (1000,)
              the cursor's fetchmany(100)  ->  2 rows
              the cursor's fetchmany(100)  ->  0 rows
            SQLCompiler.results_iter
            Model.from_db('default', ['id', 'name', 'ship_id', 'signed_on'], [3, 'Cora', 2, datetime(2011, 6, 1, 8, 0, tzinfo=UTC)], fetch_mode=FETCH_ONE)  (Sailor)
            Model.from_db('default', ['id', 'name', 'ship_id', 'signed_on'], [4, 'Dag', 2, datetime(2026, 4, 10, 9, 0, tzinfo=UTC)], fetch_mode=FETCH_ONE)  (Sailor)
      the loop's body runs with <Sailor: Cora>
      the loop's body runs with <Sailor: Dag>
    heavy._result_cache  ->  None

When the outer queryset’s filter is built, the inner one is asked to resolve itself as an expression, and QuerySet.resolve_expression hands back a copy of its query, resolved, with the queryset’s own alias for a database set on the copy. The copy is compiled into the outer statement as a subquery, and the inner queryset still has no result cache when the outer one has been evaluated. How a query becomes a subquery is in From QuerySet to SQL.

What empties the cache

A result cache is a list made at one moment, and nothing ties it to the rows it was made from. A queryset evaluated before a row was changed elsewhere goes on returning what it read. QuerySet.update, QuerySet._update and QuerySet.delete, each of which changes rows through the queryset, also set the cache of the queryset they are called on back to None (Creating, updating and deleting through a queryset). A clone has no cache to begin with, so QuerySet.all of an evaluated queryset is one that will send the statement again.