QuerySet Deep Dive: Q, F & Lookups

A QuerySet is a lazy, chainable collection of database rows that Django builds up as Python objects and only sends to the database as a single SQL query when you finally iterate, slice, or evaluate it.

Learn QuerySet Deep Dive: Q, F & Lookups in our free Django course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a…

Part of the free Django course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

In this lesson you'll master the tools that make QuerySets powerful: field lookups like __gte and __icontains, Q objects for OR/AND/NOT logic, F expressions for field-to-field comparisons and atomic updates, and the laziness that ties it all together.

What You'll Learn in This Lesson

1 Field Lookups & Chaining filter() / exclude()

A field lookup is the double-underscore suffix you attach to a field name to say how to compare it. filter() keeps rows that match, exclude() drops them, and because every QuerySet method returns a new QuerySet you can chain them. Keyword arguments inside one call are always combined with AND .

Each lookup maps directly to a piece of the generated SQL WHERE clause:

2 Q Objects for OR, AND & NOT

Plain filter() arguments can only be AND-ed together, so for OR logic you wrap each condition in a Q object and combine them with | (OR), & (AND), and ~ (NOT). A Q is a reusable bundle of conditions you can build up and pass around.

You can also build a Q up dynamically and only run the query once it is complete:

3 F Expressions & QuerySet Laziness

An F expression refers to a field's current value in the database , so the work happens in SQL rather than Python. That unlocks two things: field-to-field comparisons (compare one column to another) and atomic updates that are safe even when many requests run at once.

The reason chaining all of this is cheap is that a QuerySet is lazy . Building it does nothing; the database is only hit when you force evaluation:

🎯 Your Turn: Force a Lazy QuerySet to Evaluate

Fill in the blank so the lazy query only "runs" when you force evaluation with the method that returns its rows.

Common Errors (And How to Fix Them)

Chaining .filter(a=1).filter(b=2) gives you AND, not OR, so you get far fewer rows than expected.

✅ Fix: use a Q object — filter(Q(a=1) | Q(b=2)) — for true OR logic.

You misspelled the lookup or used a single underscore, so Django reads views_gte as a field name.

✅ Fix: use a double underscore — views__gte — between the field name and the lookup.

❌ Incrementing with obj.views += 1 loses updates

Reading into Python then saving lets two concurrent requests overwrite each other's count.

✅ Fix: use an atomic update with an F expression — .update(views=F("views") + 1) .

📋 Quick Reference

Tool

What It Does

views__gte=100

Keep rows where views >= 100 (also __lte, __gt, __lt)

title__icontains="x"

Case-insensitive substring match on a field

status__in=[...]

Match any value in the list; __isnull tests for NULL

Q(a=1) | Q(b=2)

OR logic; use & for AND and ~ for NOT

F("views") + 1

Reference a field's DB value for atomic updates

🏆 Mini Challenge: Combine Lookups, Q & F

Put it all together: select the posts that are featured OR popular, then bump each one's views the way an F expression would in a single atomic update.

❓ Frequently Asked Questions

Lesson complete — you can write expressive, efficient queries!

You can now reach for field lookups like __gte and __icontains , chain filter() and exclude() , express OR/AND/NOT with Q objects, compare and atomically update fields with F expressions, and explain why a QuerySet stays lazy until it is evaluated.

🚀 Up next: Aggregation & Annotation — summarise and compute over whole QuerySets with Count, Sum, and annotate().

Practice quiz

What is a field lookup in Django?

  • A double-underscore suffix describing how to compare a field
  • A type of database index
  • A model method
  • A URL pattern

Answer: A double-underscore suffix describing how to compare a field. Lookups like views__gte are double-underscore suffixes that map to SQL comparisons.

Which lookup performs a case-insensitive substring match?

  • __contains
  • __icontains
  • __iexact
  • __startswith

Answer: __icontains. __icontains is a case-insensitive substring match (SQL LIKE, case-insensitive).

How are multiple keyword arguments inside a single filter() call combined?

  • With OR
  • Randomly
  • With AND
  • They overwrite each other

Answer: With AND. Keyword arguments in one filter() call are always combined with AND.

Which object lets you express OR logic in a query?

  • An F expression
  • A Manager
  • A lookup
  • A Q object

Answer: A Q object. Q objects combined with | express OR logic, which plain kwargs cannot.

Which operator combines two Q objects with OR?

  • |
  • &
  • ~
  • +

Answer: |. The | operator ORs two Q objects; & is AND and ~ is NOT.

What does an F expression refer to?

  • A Python variable
  • A field's current value in the database
  • A foreign key target
  • A form field

Answer: A field's current value in the database. F() references a model field's value in the database so the operation runs in SQL.

Why is .update(views=F('views') + 1) safer than reading and saving in Python?

  • It is shorter to type
  • It validates the field
  • It increments atomically in the database, avoiding race conditions
  • It skips the database

Answer: It increments atomically in the database, avoiding race conditions. The F-based update runs as one atomic SQL UPDATE, immune to concurrent overwrites.

What does ~Q(status='draft') express?

  • status equals draft
  • status is null
  • status in a list
  • NOT (status equals draft)

Answer: NOT (status equals draft). The ~ operator negates a Q object, producing a NOT condition.

When does a lazy QuerySet actually hit the database?

  • When you iterate, slice, or call list() on it
  • As soon as you call filter()
  • When the module is imported
  • Never

Answer: When you iterate, slice, or call list() on it. Building/chaining runs no SQL; evaluation happens on iteration, slicing, list(), etc.

Which lookup tests whether a field is NULL?

  • __none
  • __isnull
  • __empty
  • __null

Answer: __isnull. published_at__isnull=True matches rows where the column IS NULL.

Continue this course