Lesson 15 / 26

QuerySets: Filtering and Laziness

Chainable, lazy queries.

filter, exclude, get, lookups

A QuerySet represents a query; methods like filter, exclude, order_by and values_list return new QuerySets without touching the database. The query runs when you iterate, slice with a step, call list, count, exists or get. Field lookups use double underscores (price__lt, category__name), including across relations. get returns exactly one object or raises DoesNotExist or MultipleObjectsReturned.

Expressive, lazy, efficient

QuerySets build SQL lazily; select_related, aggregation and F expressions keep queries efficient.

Three ideas: QuerySets, avoiding N+1, aggregation and F expressions.
Figure 5.1 — QuerySets, N+1 and aggregation.

Building and running queries, run

I ran this script with python manage.py shell --no-imports in the demo project (Django 6.1.1, SQLite). The filter builds SQL without running it (the WHERE clause is printed, with the model's default ordering); iterating runs it. get returns one product, a lookup across the relation finds in-stock Home products, and exclude counts one product out of stock.

from decimal import Decimal
from catalog.models import Category, Product

Product.objects.all().delete(); Category.objects.all().delete()
office = Category.objects.create(name="Office")
home = Category.objects.create(name="Home")
Product.objects.bulk_create([
    Product(category=office, name="Pen", price=Decimal("899")),
    Product(category=office, name="Notebook", price=Decimal("120")),
    Product(category=home, name="Lamp", price=Decimal("1499")),
    Product(category=home, name="Mug", price=Decimal("250"), in_stock=False),
])
qs = Product.objects.filter(price__lt=1000)          # lazy: no query yet
print("SQL:", str(qs.query).split(" WHERE ")[1])
print([p.name for p in qs])                           # the query runs here
print(Product.objects.get(name="Lamp"))
print(Product.objects.filter(category__name="Home", in_stock=True).values_list("name", flat=True)[:])
print(Product.objects.exclude(in_stock=True).count(), "out of stock")

Output:

SQL: "catalog_product"."price" < 1000 ORDER BY "catalog_product"."name" ASC
['Mug', 'Notebook', 'Pen']
Lamp (1499.00)
<QuerySet ['Lamp']>
1 out of stock

Use exists() and count() wisely

exists() is cheaper than fetching rows just to check presence; avoid len(qs) when you only need a count.

Quick check: When does a QuerySet hit the database?

  • When it is evaluated, for example iterated, listed, counted or fetched with get
  • When filter() is called
  • When the model is imported
  • Never
Answer

When it is evaluated, for example iterated, listed, counted or fetched with get — QuerySets are lazy.