Chapter 1 · A request, end to end
1.4 A queryset becomes SQL
How the view’s queryset, the entries whose date falls in a year, ordered by date, is built without touching the database, and what happens when the template’s loop finally evaluates it: a Query is compiled to one "SELECT" by a SQLCompiler, a connection is opened, the rows are fetched, and each becomes a model instance.
The ORM’s first rule is that building a queryset and running it are two different moments. The view builds one and hands it to the template; the template runs it. The recording shows the two moments apart. The first is in the view:
BaseManager.get_queryset (Entry.objects)
QuerySet.filter(published__year=2026)
QuerySet._clone
Query.clone
Query.add_q
Query.build_filter
Query.solve_lookup_type('published__year')
Query.setup_joins(['published'])
Query.build_lookup
Query.try_transform('year')
ExtractYear.__init__
YearExact.__init__(lhs=ExtractYear, rhs=2026)
QuerySet.order_by('published')
QuerySet._clone
Query.clone
Query.add_ordering
A manager hands out querysets
The model’s objects is the manager the metaclass gave it when its class statement ran, because the class declared none. On the class it is a ManagerDescriptor, which refuses to be read on an instance and returns the manager. A manager has few methods of its own: BaseManager makes a new QuerySet for the model, and the public methods of QuerySet, filter among them, are copied onto the manager class as methods that call them on a fresh queryset, all but the few the queryset keeps to itself and those the manager defines. So a call of filter on the manager is get_queryset followed by QuerySet.filter, which is the recording’s first two lines (Managers).
A new QuerySet holds a new Query for its model, no results, and the class that will turn rows into objects, ModelIterable. The Query is the ORM’s description of a statement to be: the tables it joins, the conditions, the ordering, what to select.
Each method returns a new queryset
QuerySet.filter does not change the queryset it is called on. It clones it, applies the filter to the clone, and returns the clone, so that a queryset can be kept and refined into several others. The clone has its own Query, which Query.clone makes by copying the mutable parts, the where clause among them. The recording’s QuerySet._clone and Query.clone under each method are this.
The keyword arguments become a Q, and Query.add_q adds it to the where clause. The work is in Query, which turns one "published__year" with its value into a condition:
Querysplits the name at the double underscores and walks the model’s fields with the pieces:.solve_lookup_type "published"is a field, aDateField;"year"is not, so it is left over as a lookup name.Queryfollows the field path, which for a field on the model itself is no join at all, and names the field and the table it is reached through;.setup_joins build_filterthen asks the field for its column, aColnaming the table and the field.Queryapplies the rule for the leftover names. A transform makes an expression out of another, as the year of a date; a lookup compares an expression with a value. Each name but the last is a transform, the last is a lookup if the column has one so named, and otherwise it is a transform too and the lookup is.build_lookup "exact". The date field has no lookup named"year", but it has a transform,ExtractYear, whichQuerywraps around the column; the transform’s.try_transform "exact"lookup isYearExact, and it is made with the transform on its left and2026on its right.
The value is prepared as it is stored: Lookup.__init__ asks the output field of its left side to prepare the right, and the transform’s output is an integer, so the value goes through IntegerField, which makes an int of what it is given; the URL converter had already made the year one.
QuerySet is simpler. It clones, clears the ordering, and Query checks that each name is a field and stores the names themselves; they are resolved to columns only when the statement is compiled. At the end of the view the queryset is a Query with one base table, one condition and one ordering, and the database has not been consulted.
The template runs it
Nothing evaluates a queryset but a request for its contents: iterating it, taking its length, testing it for truth. The for tag takes its length first (The template is rendered), and that is the second moment:
QuerySet.__len__
QuerySet._fetch_all
ModelIterable.__iter__
Query.get_compiler('default')
SQLCompiler.execute_sql
SQLCompiler.as_sql
SQLCompiler.get_select
SQLCompiler.get_order_by
Query.setup_joins(['published'])
SQLCompiler.get_from_clause
WhereNode.as_sql
YearLookup.as_sql (YearExact)
BaseDatabaseWrapper.cursor
BaseDatabaseWrapper.connect
DatabaseWrapper.get_new_connection (sqlite3)
signal connection_created -> no receivers
CursorWrapper.execute
SQL SELECT "journal_entry"."id", "journal_entry"."title", "journal_entry"."published" FROM "journal_entry" WHERE "journal_entry"."published" BETWEEN %s AND %s ORDER BY "journal_entry"."published" ASC with params ('2026-01-01', '2026-12-31')
SQLiteCursorWrapper.execute
SQLCompiler.results_iter
DatabaseOperations.convert_datefield_value (sqlite3)
Model.from_db (Entry)
Model.__init__ (Entry)
signal pre_init -> no receivers
signal post_init -> no receivers
DatabaseOperations.convert_datefield_value (sqlite3)
Model.from_db (Entry)
Model.__init__ (Entry)
signal pre_init -> no receivers
signal post_init -> no receivers
QuerySet.__iter__
QuerySet._fetch_all
QuerySet fills the result cache if there is none yet, with the list of what the iterable class yields; iterating a queryset, indexing it, taking its length and testing it for truth all go through it, and QuerySet is the method for reading rows without keeping them. The loop’s QuerySet a moment later finds the cache filled and runs no query: a queryset is evaluated once and holds its rows.
ModelIterable does the whole of one query. It asks the router which database to read from, which with no routers configured is "default"; asks the Query for a compiler for that database’s backend, which the backend’s operations object supplies by name; runs it; and turns each row into an instance.
Compiling the statement
SQLCompiler first calls SQLCompiler, which is where the Query becomes a string and its parameters. The compiler builds the clauses in an order that is not the statement’s: the select columns, which with nothing deferred are every concrete field of the model, each a Col; the ordering, which is when the name stored by order_by is resolved to a column, by SQLCompiler, the recording’s second Query; the from clause from the tables the query references; and the where clause. It then joins them as "SELECT", "FROM", "WHERE", "ORDER BY", appending each clause’s parameters in the same order, so that the placeholders and the parameters line up.
Each node of the query writes its own SQL. SQLCompiler calls a node’s as_sql, or a vendor-specific method such as as_sqlite when the node defines one for this backend. The where clause compiles its children and joins them with "AND". Its one child is the year lookup, and its as_sql is the reason the recorded statement compares the column itself and extracts no year from it: when the right side is a plain value, YearLookup compiles the column under the transform instead of the transform, and compares it with "BETWEEN" the first and last day of the year, which lets the database use an index on the column. The two dates are the statement’s two parameters.
The connection and the cursor
The statement needs a cursor, and BaseDatabaseWrapper opens the connection if there is none. The wrapper object itself lives as long as the thread: connections keeps one DatabaseWrapper per database alias per thread, and what BaseDatabaseWrapper opens and close discards is the driver’s connection inside it. connect resets the transaction state, notes when the connection should be closed, from the database’s "CONN_MAX_AGE", calls the backend’s get_new_connection, sets autocommit, and sends connection_created. The SQLite backend’s get_new_connection connects with the standard library’s module, registers the SQL functions Django implements in Python, and turns foreign key enforcement on with a pragma.
With "AUTOCOMMIT" true, the default, and "ATOMIC_REQUESTS" false, no transaction is opened around the query: each statement is a transaction of its own, which the database commits as it ends, and on SQLite autocommit means the driver issues no "BEGIN" of its own (DatabaseWrapper; Transactions: autocommit, atomic and savepoints). The cursor is wrapped in a CursorWrapper, or in CursorDebugWrapper when BaseDatabaseWrapper is true, which it is when settings.DEBUG is or when a test is capturing queries; that is the wrapper that logs every query, and the recording ran with the setting off. CursorWrapper runs any execute wrappers the project installed around the real call, refuses to run inside a transaction that needs a rollback, and translates the driver’s exceptions into Django’s. The SQLite cursor’s last act is to rewrite the %s placeholders Django writes into the ? the driver wants.
All of this is one statement through Django’s cursor; the pragmas, and a question about SQLite’s own functions, ran on the driver’s connection as it was opened, below the cursor. The recording writes down what passes through the cursor, one "SELECT" per request and no "BEGIN", which would have passed through it too; with autocommit on, no commit is issued either.
Rows become objects
SQLCompiler reads the rows the cursor fetched and applies the converters the backend declares for each column’s type. SQLite has no date type, so for a DateField the backend supplies DatabaseOperations, which parses a date that arrived as text; here the column’s text was already parsed on the way out of the driver, by a converter the backend registers with the driver when it is imported, so the backend’s own converter finds a date and leaves it.
For each converted row Model.from_db makes the instance: it calls the class with the values in field order, then marks the instance as not being added and records the database it came from. Model.__init__ sends pre_init, sets each field from the positional values, and sends post_init; the two signals are sent for every row, receivers or none. These instances are what the loop iterates, and what "entry.title" resolves against in the template (An instance and its state).
When the request ends the connection is closed, as The response returns to the server says. The wrapper, SQLite’s DatabaseWrapper, stays on the thread, and the next request’s first query opens a connection again, which is why the second request in the recording also shows BaseDatabaseWrapper.