Chapter 1: QuerySet Introduction
QuerySet Introduction
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” — they loop over them, slice them, print them, and think “okay, it’s a list”. But QuerySets are not lists. They are lazy database query builders — very clever, chainable, SQL-optimizing promises that don’t actually hit the database until you force them to.
Understanding QuerySets properly is the single biggest thing that separates “Django code that works” from “Django code that is fast, clean, scalable, and professional”.
We are going to learn this slowly, step-by-step, like pair-programming — I’ll explain every line, when SQL runs, what happens in memory, common traps, and the patterns you’ll use 50 times a day in real projects.
We’ll do almost everything in the Django shell so you see exactly what’s happening.
Step 1: Open the Shell & Import Models
|
0 1 2 3 4 5 6 |
python manage.py shell |
|
0 1 2 3 4 5 6 7 8 |
from polls.models import Question, Choice from django.utils import timezone from django.db.models import Sum, Count, Q |
Assume you already have some questions and choices in the database (from admin or earlier shell sessions).
Step 2: The Most Basic QuerySet – .all()
|
0 1 2 3 4 5 6 7 8 |
qs = Question.objects.all() print(qs) # <QuerySet [<Question: What's your favorite…>, …]> print(type(qs)) # <class 'django.db.models.query.QuerySet'> |
Important facts:
- qs is not a list — it is a QuerySet object
- No SQL has run yet — Django has only prepared the query
- You can print it → it forces evaluation → now SQL runs: SELECT * FROM polls_question
|
0 1 2 3 4 5 6 7 8 9 10 |
list(qs) # → forces evaluation, returns real Python list len(qs) # → runs COUNT(*) query (optimized) qs.exists() # → runs EXISTS subquery (very fast) bool(qs) # → same as .exists() qs.count() # → explicit COUNT(*) |
Key takeaway: QuerySets are lazy — they delay database access until you force them (loop, len, list(), print, indexing, etc.).
Step 3: Chaining – The Real Magic
Every method that filters / orders / annotates returns a new QuerySet — you can chain forever:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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", "slug", "pub_date")[:10] print(qs) # now SQL runs |
This generates one efficient SQL query:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
SELECT "polls_question"."id", "polls_question"."question_text", "polls_question"."slug", "polls_question"."pub_date" FROM "polls_question" WHERE "polls_question"."is_active" = TRUE AND "polls_question"."pub_date" >= '2026-01-01' AND NOT ("polls_question"."category" = 'politics') ORDER BY "polls_question"."pub_date" DESC LIMIT 10 |
Important: chaining is immutable — qs.filter() returns new object — original qs unchanged
Step 4: Most Common Methods You Will 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 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# Start point Question.objects.all() # everything # Filtering Question.objects.filter(is_active=True) Question.objects.filter(pub_date__year=2026) Question.objects.filter(category__in=["fun", "sports"]) Question.objects.filter(question_text__icontains="biryani") # case-insensitive contains Question.objects.filter(vote_count__gt=5) # note: vote_count needs annotation # Exclude Question.objects.exclude(category="politics") # Ordering Question.objects.order_by("-pub_date") # newest first Question.objects.order_by("category", "-pub_date") # Slicing (LIMIT / OFFSET) Question.objects.all()[:5] # first 5 Question.objects.all()[5:10] # 6th to 10th Question.objects.all().order_by("?")[:3] # random 3 # Values (return dicts instead of objects – faster) Question.objects.values("id", "question_text") # [{'id':1, 'question_text':…}, …] # Values list (flat tuples) Question.objects.values_list("id", flat=True) # [1, 2, 3, …] # Only / defer (fetch only needed columns) Question.objects.only("question_text", "slug") Question.objects.defer("big_text_field") # First / last / get Question.objects.filter(slug="best-biryani").first() # None if none Question.objects.get(slug="best-biryani") # raises DoesNotExist get_object_or_404(Question, slug="best-biryani") # raises Http404 |
Step 5: Aggregates & Annotations (Very Powerful)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# Count total questions Question.objects.count() # fast COUNT(*) # Annotate vote count per question qs = Question.objects.annotate( total_votes=Sum("choices__votes") ).order_by("-total_votes") for q in qs: print(q.question_text, q.total_votes) |
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
# Aggregate across all stats = Question.objects.aggregate( total_questions=Count("id"), avg_votes=Avg("choices__votes"), max_votes=Max("choices__votes") ) print(stats) # {'total_questions': 42, 'avg_votes': 3.5, …} |
Step 6: Avoiding N+1 Queries (The #1 Performance Killer)
Bad (slow – N+1 problem):
|
0 1 2 3 4 5 6 7 8 |
questions = Question.objects.all()[:10] for q in questions: print(q.choices.all()) # extra query per question! |
Good:
|
0 1 2 3 4 5 6 7 |
questions = Question.objects.prefetch_related("choices").all()[:10] # 2 queries total: one for questions, one for all related choices |
|
0 1 2 3 4 5 6 7 |
# If ForeignKey (e.g. choice.question) choices = Choice.objects.select_related("question").all() |
Step 7: Your Quick Practice Task (Do This in Shell Now)
- from polls.models import Question, Choice
- qs = Question.objects.filter(is_active=True).order_by(“-pub_date”)[:5]
- print(qs) → see objects
- qs.count() → fast count
- qs.annotate(vote_count=Sum(“choices__votes”)) → add vote_count
- for q in qs: print(q.question_text, q.vote_count)
- Try qs.filter(pub_date__year=2026)
Tell me what feels next:
- Which QuerySet part is still confusing? (chaining? lookups? N+1? annotation?)
- Want 20 more real-life QuerySet examples from different scenarios?
- Ready to learn Q objects (complex OR conditions)?
- Or finally ready for Forms + Voting + POST + F() expression?
You’re now starting to think like Django ORM pros — this QuerySet mastery is the single biggest skill upgrade after models themselves.
Keep playing in shell — you’re doing fantastic! 🚀🇮🇳
