F() and Q(): database-side maths and complex lookups
Two small tools unlock a large amount of the ORM's real power: F(), which lets you refer to a database
column inside a query so the database does the arithmetic, and Q(), which lets you build OR and
complex boolean conditions that plain keyword-argument filtering cannot express. Both keep work in the
database where it belongs, and both fix bugs that beginners hit constantly. This lesson is when and how to
use each.
F(): refer to a column, compute in the database
Normally a filter compares a column to a Python value: filter(fee__gt=600). But sometimes you need to
compare a column to another column, or update a column based on its own current value. That is what
F() is for — it means "the value of this field, in the database":
from django.db.models import F
# raise every appointment fee by 10%, in the database
Appointment.objects.update(fee=F("fee") * Decimal("1.1"))
# verified: total went from 3500 to 3850 (3500 * 1.1)
The critical thing: F("fee") * 1.1 is not computed in Python. It generates SQL —
UPDATE ... SET fee = fee * 1.1 — so the database reads each row's current fee and multiplies it, in one
statement, without loading a single row into Python. Compare the naive version:
for a in Appointment.objects.all(): # loads every row
a.fee = a.fee * Decimal("1.1")
a.save() # one UPDATE per row — N queries
The F() version is one query; the loop is N queries and loads all the data. On real volumes that is the
difference between instant and painfully slow.
F() also fixes the race condition in read-modify-write
There is a subtler, more important reason to use F(): it avoids a race condition. Consider
incrementing a counter the naive way:
appt = Appointment.objects.get(id=1)
appt.fee = appt.fee + 100 # reads the value into Python...
appt.save() # ...and writes it back
Between the read and the write, another request could change the same row — and your write, based on a
stale value, silently overwrites theirs. This is the classic lost update. With F():
Appointment.objects.filter(id=1).update(fee=F("fee") + 100)
the increment happens atomically in the database — SET fee = fee + 100 reads and writes in one
operation, so two concurrent increments both count. F() is not just faster; for any "adjust a value based
on its current value" it is the correct tool, and the naive read-modify-write is a genuine bug waiting for
concurrency. (The concurrency module returns to this race in depth.)
F() for column-to-column comparisons
F() also lets a filter compare two fields of the same row:
# appointments where the fee exceeds the doctor's standard rate (if such a field existed)
Appointment.objects.filter(fee__gt=F("doctor__standard_fee"))
# patients created and updated at different times
Patient.objects.exclude(created_at=F("updated_at"))
You cannot express "this column versus that column" with a plain value on the right-hand side — F() is
what puts a column there. Any time your condition involves two fields rather than a field and a constant,
F() is the answer.
Q(): OR, and complex boolean logic
Plain filtering combines conditions with AND: filter(status="booked", fee__gt=600) means booked and
expensive. But you cannot write OR that way — there is no keyword-argument syntax for "this OR that".
Q() objects fix that:
from django.db.models import Q
# booked OR fee greater than 600
Appointment.objects.filter(Q(status="booked") | Q(fee__gt=600))
# verified count: 5
Q objects combine with | (OR), & (AND), and ~ (NOT), and you can group them with parentheses like
any boolean expression:
# (booked OR done) AND not cheap
Appointment.objects.filter((Q(status="booked") | Q(status="done")) & ~Q(fee__lt=100))
This is how you express any condition more complex than a flat list of ANDs. A dashboard filter like "show
appointments that are either overdue or unpaid" is a Q(...) | Q(...) — impossible with keyword arguments
alone.
Mixing Q() and keyword arguments
You can pass Q objects and keyword arguments to the same filter, but the Q objects must come
first (Python requires positional arguments before keyword arguments):
Appointment.objects.filter(Q(status="booked") | Q(fee__gt=600), doctor=some_doctor)
# (booked OR expensive) AND for this doctor
The keyword argument (doctor=some_doctor) is ANDed with the whole Q expression. This mix — an OR
condition further narrowed by a simple equality — is extremely common in real filters.
The one rule that ties both together
F() and Q() share a philosophy with the rest of this module: keep the work in the database. F()
does arithmetic and comparisons on columns without shipping rows to Python (and does read-modify-write
safely); Q() expresses boolean logic the database evaluates directly. Reaching for a Python loop to do
what F() does, or fetching-then-filtering in Python what Q() could ask directly, is the slow, sometimes
buggy path. When you find yourself looping to adjust values, or filtering results in Python because "you
can't do OR in a filter" — you can, and these two tools are how.
Check your work
What F() means. "The value of this field in the database" — so arithmetic and comparisons run in SQL,
not Python. Verified: update(fee=F("fee") * 1.1) took the total 3500 → 3850 in one query.
Why F() beats read-modify-write. It updates atomically in the database (SET fee = fee + 100),
avoiding the lost-update race and the N-query loop that loads every row.
F() for column comparisons. It puts a column on the right-hand side of a filter, so you can compare
two fields of the same row (fee__gt=F("doctor__standard_fee")).
What Q() enables. OR and complex boolean logic — | (OR), & (AND), ~ (NOT), grouped with
parentheses — which plain keyword-argument filtering (AND-only) cannot express. Verified OR count: 5.
Mixing Q and kwargs. Q objects come first, then keyword arguments; the kwargs are ANDed with the
whole Q expression.
The shared principle. Keep work in the database — F() for column maths/safe updates, Q() for boolean
logic — rather than looping or filtering in Python.
Practice
- Reproduce the
F()fee raise:update(fee=F("fee") * Decimal("1.1")); confirm the total goes 3500 → 3850 in one query (checkconnection.queries). - Write the same raise as a Python loop with
.save(); compare the query counts and reason about the difference at scale. - Increment one appointment's fee with
F("fee") + 100via.update(); explain in one sentence why this is safe against concurrent updates and the read-modify-write version is not. - Reproduce the
Q(status="booked") | Q(fee__gt=600)filter; confirm the count is 5 and work out by hand why. - Write a
~Q(...)(NOT) filter and a grouped(Q | Q) & Qfilter; confirm the results match your reasoning. - Combine a
QOR-expression with a keyword argument in onefilter; deliberately put the kwarg first and read the PythonSyntaxError, then fix the order.
Official documentation
- Django —
F()expressions — Column references and database-side arithmetic. - Django — Complex lookups with
Qobjects — OR, AND, NOT and grouping. - Django — Avoiding race conditions using
F()— WhyF()updates are atomic.
Next: custom managers and querysets — naming your queries.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship