What Is a QuerySet?
A QuerySet represents a collection of database rows that Django can filter, order, and slice, but critically it is lazy — the SQL query is not executed until the QuerySet is evaluated, such as by iterating over it, calling list() on it, or accessing its length. This laziness lets you chain methods like filter(), exclude(), and order_by() to build up a single, efficient SQL query instead of hitting the database multiple times. Model.objects is the default Manager that provides the entry point for building QuerySets, for example Book.objects.filter(price__lt=20).
Cricket analogy: A lazy QuerySet is like a batting order announced before a match but not locked until the toss — you can still swap Kohli's position in the lineup right up until the umpire calls play, just as filter() chains modify the query until it's actually evaluated.
Filtering and Field Lookups
Django's field lookups extend filter() and exclude() with double-underscore suffixes like __gt, __lt, __contains, __icontains, and __in to build WHERE clauses without writing SQL. Lookups can also traverse relationships, so Book.objects.filter(author__country='India') follows a foreign key join automatically. Q objects from django.db.models allow OR logic and complex boolean combinations that plain keyword arguments to filter() cannot express, since keyword arguments are always ANDed together.
Cricket analogy: author__country='India' traversing a foreign key is like a stats site letting you filter batsmen not just by their own stats but by which country's team they play for — the lookup follows the relationship one hop out.
from django.db.models import Q
# Chained filtering — builds a single SQL query, lazily
cheap_available_books = (
Book.objects
.filter(is_available=True, price__lt=20)
.exclude(title__icontains="draft")
.order_by("-published_date")
)
# Q objects for OR logic across fields
urgent_books = Book.objects.filter(
Q(price__lt=10) | Q(is_available=False)
)
# Traversing a relationship in a lookup
indian_authors_books = Book.objects.filter(author__country="India")Aggregation, Annotation, and Query Optimization
aggregate() computes a single summary value across a QuerySet, such as Book.objects.aggregate(Avg('price')), while annotate() attaches a computed value to each row in the result, useful for things like counting related objects per author with Count('book'). select_related() and prefetch_related() solve the N+1 query problem: select_related() performs a SQL JOIN for forward foreign-key and one-to-one relationships in a single query, while prefetch_related() issues a separate optimized query for many-to-many and reverse foreign-key relationships and joins the results in Python.
Cricket analogy: annotate(Count('century')) per batsman is like a stats board computing each player's total centuries alongside their name in one pass, rather than looking up each player's century count in a separate query one at a time.
QuerySets are evaluated (i.e., the SQL actually runs) when you iterate over them, call len(), slice with a step, call list(), or use them in an if statement — bool(), repr(), and pickling also trigger evaluation. Until then, chaining .filter() or .order_by() only builds up the query object.
Looping over a QuerySet and accessing a related object inside the loop (e.g., for book in Book.objects.all(): print(book.author.name)) triggers one extra query per iteration — the classic N+1 problem. Use select_related('author') to collapse it into a single JOIN query.
- QuerySets are lazy: chaining filter(), exclude(), and order_by() builds a query object that only hits the database on evaluation.
- Field lookups like __gt, __icontains, and __in extend filter()/exclude() into WHERE clause conditions without raw SQL.
- Keyword arguments passed to filter() are ANDed together; use Q objects to express OR logic or complex boolean combinations.
- Lookups can traverse relationships with double underscores, e.g., author__country='India'.
- aggregate() returns a single summary value; annotate() attaches a computed value to each row.
- select_related() JOINs forward/one-to-one relationships in one query; prefetch_related() optimizes many-to-many and reverse foreign-key lookups with a separate query.
- Failing to use select_related/prefetch_related when accessing related objects in a loop causes the N+1 query problem.
Practice what you learned
1. When does a Django QuerySet actually execute its SQL query?
2. How are multiple keyword arguments passed to a single filter() call combined?
3. What problem does select_related() primarily solve?
4. Which QuerySet method would you use to compute a single average price across all books?
5. What is the correct way to express 'price less than 10 OR is_available is False' in a QuerySet?
Was this page helpful?
You May Also Like
Defining Models
How to declare Django models as Python classes that map to database tables, including field types, options, and Meta configuration.
Model Relationships
How ForeignKey, ManyToManyField, and OneToOneField model real-world relationships between Django models.
Model Validation
How Django validates model data using field validators, clean methods, and full_clean(), and why validation isn't automatic on save().
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentNext.js Study Notes
JavaScript · 30 topics