🗄️

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?

  1. Chaining optimization — stack multiple conditions, execute SQL once
  2. Reuse — add different conditions to same QuerySet for multiple queries
  3. Avoid unnecessary queries — no evaluation = no SQL

N+1 Problem

select_related (JOIN) or prefetch_related (separate query + Python combination) to solve.

Key Points

1

filter/exclude/order_by don't execute SQL — just stack conditions

2

SQL executes only on list(), for, [0] access (lazy evaluation)

3

Internally Query object manages condition tree → as_sql() generates SQL string

4

N+1 solved with select_related (JOIN) or prefetch_related (separate query)

Use Cases

Complex search — combine conditional filters via chaining API pagination — slicing QuerySet generates LIMIT/OFFSET SQL