RizTech Academy logo
RizTech Academy
The ORM in DepthLesson 2 of 630 min

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

  1. Reproduce the F() fee raise: update(fee=F("fee") * Decimal("1.1")); confirm the total goes 3500 → 3850 in one query (check connection.queries).
  2. Write the same raise as a Python loop with .save(); compare the query counts and reason about the difference at scale.
  3. Increment one appointment's fee with F("fee") + 100 via .update(); explain in one sentence why this is safe against concurrent updates and the read-modify-write version is not.
  4. Reproduce the Q(status="booked") | Q(fee__gt=600) filter; confirm the count is 5 and work out by hand why.
  5. Write a ~Q(...) (NOT) filter and a grouped (Q | Q) & Q filter; confirm the results match your reasoning.
  6. Combine a Q OR-expression with a keyword argument in one filter; deliberately put the kwarg first and read the Python SyntaxError, then fix the order.

Official documentation

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