QuerySets
QuerySets: (the thing you get when you write Question.objects.all(), filter(), order_by(), etc.)
Most beginners treat QuerySets like “just a list of objects” — but they are much smarter, lazy, chainable, SQL-optimizing magic objects. Understanding them deeply will make your code 10× faster, cleaner, and more professional.
I’m going to teach this like a senior sitting next to you — slowly, with real examples from your polls app, showing exactly what happens behind the scenes, when queries are executed, how to avoid N+1 problems, how to chain safely, and the 20 most common patterns you’ll use every day.
Let’s open python manage.py shell together and play.
1. What Actually Is a QuerySet?
|
0 1 2 3 4 5 6 7 8 |
from polls.models import Question qs = Question.objects.all() # ← this is a QuerySet |
- It is not a list — it is a promise to run SQL later
- It is lazy — no database hit until you force evaluation (e.g. list(qs), for loop, len(qs), qs[0], .exists(), etc.)
- It is immutable in chain — every method like .filter() returns new QuerySet
|
0 1 2 3 4 5 6 7 8 9 |
qs = Question.objects.all() print(qs) # <QuerySet []> or [<Question: …>, …>] # Nothing hit DB yet! |
2. Forcing Evaluation (When SQL Actually Runs)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
list(qs) # → runs SELECT * FROM polls_question len(qs) # → runs COUNT qs[0] # → runs LIMIT 1 for q in qs: … # → runs full query qs.exists() # → runs EXISTS subquery (fast!) bool(qs) # → same as exists() print(qs) # → evaluates for repr |
Golden rule — never evaluate large QuerySets accidentally in templates or admin:
Bad:
|
0 1 2 3 4 5 6 7 8 |
{% for q in Question.objects.all %} … {% endfor %} |
Good:
|
0 1 2 3 4 5 6 7 8 9 10 |
# in view context["latest_questions"] = Question.objects.filter(is_active=True).order_by("-pub_date")[:10] # template {% for q in latest_questions %} |
3. Chaining Methods (The Real Power)
Every chain method returns new QuerySet — you can chain forever:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
qs = Question.objects \ .filter(is_active=True) \ .filter(pub_date__gte=timezone.now() - timezone.timedelta(days=30)) \ .exclude(category="politics") \ .order_by("-pub_date") \ .select_related() \ .only("question_text", "pub_date", "slug")[:20] |
Common chain methods:
| Method | Purpose | Returns new QuerySet? | Executes SQL? |
|---|---|---|---|
| .all() | Start point | Yes | No |
| .filter(**kwargs) | WHERE conditions | Yes | No |
| .exclude(**kwargs) | NOT conditions | Yes | No |
| .order_by(“field”) | ORDER BY | Yes | No |
| .values(“field1”) | Return dicts instead of objects | Yes | No |
| .values_list(“field”) | Return flat tuples | Yes | No |
| .only(“field1”) | Only fetch these columns | Yes | No |
| .defer(“big_field”) | Fetch all except these | Yes | No |
| .select_related() | JOIN related objects (ForeignKey) | Yes | No |
| .prefetch_related() | Separate query for ManyToMany / reverse FK | Yes | No |
| .annotate() | Add computed columns | Yes | No |
| .aggregate() | Compute SUM, COUNT, AVG… | No — returns dict | Yes |
| .count() | Fast COUNT(*) | No — returns int | Yes |
| .exists() | Fast EXISTS | No — returns bool | Yes |
| .first() / .last() | LIMIT 1 | No — returns object | Yes |
| .distinct() | DISTINCT | Yes | No |
4. Lookups (The Magic in filter(field__lookup=…)) – Most Used Syntax
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Question.objects.filter( pub_date__gte=timezone.now() - timedelta(days=7), # >= pub_date__lte=timezone.now(), # <= is_active=True, category__in=["fun", "sports"], question_text__icontains="biryani", # case-insensitive contains slug__startswith="best-", vote_count__gt=5, # > (note: vote_count is method or annotation) ) |
Most common lookup suffixes (memorize these 15):
- __exact / __iexact (case-sensitive / insensitive equal)
- __contains / __icontains
- __startswith / __istartswith
- __endswith / __iendswith
- __gt / __gte / __lt / __lte
- __in (list/tuple)
- __range (between two values)
- __year / __month / __day / __week / __weekday
- __isnull / __isnotnull
- __regex / __iregex (rare but powerful)
5. Real Example – Complex QuerySet in View
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
def index(request): latest_questions = Question.objects \ .filter(is_active=True) \ .filter(pub_date__gte=timezone.now() - timezone.timedelta(days=30)) \ .exclude(category="politics") \ .order_by("-pub_date") \ .select_related() \ .annotate(vote_count=Sum("choices__votes")) \ .only("question_text", "slug", "pub_date", "category", "vote_count")[:10] return render(request, "polls/index.html", { "latest_questions": latest_questions }) |
→ Efficient: only needed columns, pre-joins, annotation, limited rows
6. Avoiding N+1 Queries (Most Common Performance Killer)
Bad (N+1 problem):
|
0 1 2 3 4 5 6 7 8 9 10 11 |
{% for question in latest_questions %} {{ question.question_text }} {% for choice in question.choices.all %} {{ choice.choice_text }} {% endfor %} {% endfor %} |
→ 1 query for questions + N queries for choices = slow
Good:
|
0 1 2 3 4 5 6 7 8 9 10 |
# in view latest_questions = Question.objects \ .filter(is_active=True) \ .prefetch_related("choices") \ .order_by("-pub_date")[:10] |
→ 2 queries total: one for questions, one for all related choices
7. Quick Reference – QuerySet Patterns You’ll Use Every Day
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# All active questions, newest first Question.objects.filter(is_active=True).order_by("-pub_date") # Last 5 …[:5] # Count votes per question .annotate(vote_count=Sum("choices__votes")) # Questions with at least 1 vote .filter(choices__votes__gt=0) # Distinct categories .values("category").distinct() # Exists check (fast) Question.objects.filter(category="fun").exists() # First matching Question.objects.filter(slug="best-biryani").first() # Or raise 404 get_object_or_404(Question, slug=slug) |
Your Quick Practice Task (Do This in Shell)
- python manage.py shell
- from polls.models import Question
- qs = Question.objects.filter(is_active=True).order_by(“-pub_date”)[:5]
- print(qs) → see objects
- list(qs) → force evaluation
- qs.count() → fast COUNT
- qs[0] → first object
- Try .annotate(vote_count=Sum(“choices__votes”)) → see extra attribute
Tell me what feels next:
- Which QuerySet part is still confusing? (chaining? lookups? N+1? annotation?)
- Want more examples for __ lookups (dates, strings, related fields)?
- Ready to learn Q objects (OR conditions, complex filters)?
- Or ready to finally build Forms + Voting + POST + F() increment?
You’re getting very strong with QuerySets now — this is the single most important skill after models themselves.
Keep playing in shell — you’re doing fantastic! 🚀🇮🇳
