100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Python

Django ORM QuerySets

How QuerySets let you retrieve, filter, and chain database queries lazily using Python instead of raw SQL.

Models & ORMIntermediate9 min readJul 10, 2026
Analogies

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.

python
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

Was this page helpful?

Topics covered

#Python#DjangoStudyNotes#WebDevelopment#DjangoORMQuerySets#Django#ORM#QuerySets#QuerySet#StudyNotes#SkillVeris