Django ORM Internals — Why QuerySets Are Lazy
Why chaining filter().exclude().order_by() doesn't execute SQL
QuerySet is "an object describing a SQL query," not the result.
Internal Structure
QuerySet has a query attribute — a django.db.models.sql.Query object storing WHERE conditions, ORDER BY, JOIN info as a tree. filter() clones the QuerySet and adds Q objects to the condition tree. SQL string isn't generated yet.
SQL is generated when query.get_compiler().as_sql() is called — when the QuerySet is "evaluated" (__iter__, __len__, list()).
Why Lazy?
- Chaining optimization — stack multiple conditions, execute SQL once
- Reuse — add different conditions to same QuerySet for multiple queries
- Avoid unnecessary queries — no evaluation = no SQL
N+1 Problem
select_related (JOIN) or prefetch_related (separate query + Python combination) to solve.
Key Points
filter/exclude/order_by don't execute SQL — just stack conditions
SQL executes only on list(), for, [0] access (lazy evaluation)
Internally Query object manages condition tree → as_sql() generates SQL string
N+1 solved with select_related (JOIN) or prefetch_related (separate query)