3 Costly Django ORM Mistakes I Made as a Beginner — and How I Fixed Them


Mistake #1: Django QuerySet Filtering Doesn’t Work on Dynamic Fields
I ran into a small but surprising behavior while working with Django QuerySets. You can't filter on dynamic fields — even if you attach them to your queryset objects. Here's what I tried, what didn’t work, and the proper way to do it.
🧪 What I Did (Code Sample)
qs = MyModel.objects.all()
for obj in qs:
obj.custom_field = some_calculated_value(obj)
# in another method
.......
qs.filter(custom_field__gt=10) # ❌ Doesn’t work!
😬 Where It Broke
Because custom_field isn’t a real database field or annotation — it’s just a dynamic Python attribute. Django ORM doesn’t know it exists.
🛠️ How to Fix It (Annotate!)
from django.db.models import F
qs = qs.annotate(
custom_field=F('service_title')
)
# in another method
.......
qs = qs.filter(service_title='cleaning')
✔ Now it works — because annotate() creates a query-level field that SQL understands.
💡Key Lessons
Django ORM only understands fields that exist at the database level or are explicitly annotated.You can't filter using values that you added dynamically in Python after the queryset is evaluated.
Adding attributes in Python (like obj.custom_field = ...) does not make them usable in filter() or order_by().They're invisible to the ORM and SQL engine.
Mistake #2: Any Operation on ManyToMany Fields Can Duplicate Your Queryset
📖 What Happened
I needed to create a field by concatenating first_name and last_name so I could filter on full names. I used Concat() from django.db.models.functions, and it worked beautifully — even filtering on it worked:
qs.annotate(full_name=Concat('diff_users__first_name', Value(' '), 'diff_users__last_name'))
In Oracle, this translates to: :
Oracle
SELECT ...,
diff_users.first_name || ' ' || diff_users.last_name AS full_name
FROM
main_table
LEFT JOIN
diff_users ON diff_users.id = main_table.diff_user_id
😬 Where It Broke
It worked fine as long as there was only one
diff_user
. Butdiff_users
is a ManyToMany relationship.So….
The number of rows in the queryset exploded.
Each duplicate row was the same object, just repeated for each related diff_users.
distinct() didn’t help because I still needed to access all diff_users for each object.
🛠️ How to Fix It (Aggregation Like ListAgg, StringAgg)
ListAgg()
To avoid duplicate rows and still show all related many-to-many values:
from django.db.models import Value, F, CharField
from django.db.models.functions import Concat
from django.db.models import Aggregate
# Minimal custom ListAgg class for Oracle
class ListAgg(Aggregate):
function = 'LISTAGG'
template = "%(function)s(%(expressions)s, '%(delimiter)s') WITHIN GROUP (ORDER BY %(ordering)s)"
output_field = CharField()
def init(self, expression, delimiter=', ', ordering=F('id')):
super().__init__(expression, delimiter=delimiter, ordering=ordering)
# Usage in queryset
qs = qs.annotate(
full_name=ListAgg( Concat(F('diff_users__first_name'), Value(' '), F('diff_users__last_name')),
delimiter=', ',
ordering=F('id')
)
)
In Oracle, this translates to:
SELECT
...,
LISTAGG(diff_users.first_name || ' ' || diff_users.last_name, ', ') WITHIN GROUP (ORDER BY diff_users.id) AS full_name
FROM
main_table
LEFT JOIN
diff_users ON diff_users.main_table_id = main_table.id
GROUP BY
main_table.id, ... -- other main_table columns
LISTAGG(expression, delimiter) WITHIN GROUP (ORDER BY ...) aggregates multiple related rows into a single concatenated string.
The || operator concatenates first and last names with a space.
The GROUP BY ensures one row per main object.
qs=qs.filter(full_name__icontains='nafisa')
StringAgg()
from django.db.models import Value, F
from django.db.models.functions import Concat
from django.contrib.postgres.aggregates import StringAgg
qs = qs.annotate(
user_names=StringAgg(
Concat(
F('musers__first_name'),
Value(' '),
F('musers__last_name')
),
delimiter=', '
)
)
In PostgreSQL this translate to:
SELECT
...,
STRING_AGG(musers.first_name || ' ' || musers.last_name, ', ') AS user_names
FROM
main_table
LEFT JOIN
musers ON musers.main_table_id = main_table.id
GROUP BY
main_table.id, -- plus any other selected columns from main_table
Depending on your database (e.g., PostgreSQL), you might use StringAgg or custom functions instead.
💡Key Lessons
If you're doing any operations on fields from a related model, and there are multiple related entries/ many to many, use StringAgg,ListAgg or database-specific aggregators instead.
Mistake #3: Multiple Filters on the Same Related Table Can Cause Extra Joins
🐛 What I did
While filtering a queryset, I added multiple filters on fields from the related Books
table:
qs = qs.filter(books__name__in=['Harry Potter', 'Dune'])
# some more code
......
qs = qs.filter(books__price__in=[500, 1000])
This looks perfectly fine — and it works — but Django internally creates separate joins to the Books
table for each filter.
In SQL, it becomes something like:
SELECT ...
FROM main_table
LEFT JOIN books AS U1 ON ...
LEFT JOIN books AS U2 ON ...
WHERE U1.name IN (...) AND U2.price IN (...)
😬 Where It Broke
My API response became noticeably slower. Django was generating multiple joins to the same related table — in this case, a large Books
table. Each additional join added overhead, as the database had to scan, compare, and match rows for every join operation. This significantly increased the computational load and slowed down the overall query execution.
🛠️ The Fix
Use a single .filter()
with both conditions together:
qs = qs.filter(
books__name__in=['Harry Potter', 'Dune'],
books__price__in=[500, 1000]
)
In SQL, it becomes something like:
SELECT ...
FROM main_table
JOIN book ON book.store_id = main_table.id
WHERE book.name IN ('Harry Potter', 'Dune') AND book.price IN (500, 1000);
This ensures Django uses only one join, and filters books that satisfy both conditions.
🔑 Key Lessons
Multiple filters on the same reverse relation can trigger multiple joins.
If your intent is to match conditions on the same related record, combine filters in a single
.filter(...)
call.
Subscribe to my newsletter
Read articles from Nafisa Alam directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Nafisa Alam
Nafisa Alam
I really love to develop stuff that solves problems in our everyday life. I am interested in opening an all-female tech supporting platform in Bangladesh. Anyone with the same interest can contact me.