Most "slow database" complaints in Django are not the database's fault. They are the ORM issuing hundreds of tiny queries where one would do. You can fix almost all of it without writing a single line of raw SQL. Here is how.
1. Kill N+1 Queries with select_related
Accessing a foreign key in a loop triggers one query per row. select_related performs a SQL join and fetches everything in a single query.
# Slow: 1 query for books, then 1 per book for the author for book in Book.objects.all(): print(book.author.name) # Fast: one JOIN for book in Book.objects.select_related("author"): print(book.author.name)
Use select_related for forward ForeignKey and OneToOne relationships.
2. Use prefetch_related for Many-to-Many
Joins do not work for reverse or many-to-many relations. prefetch_related runs a second query and stitches the results together in Python.
for author in Author.objects.prefetch_related("books"): print(author.name, [b.title for b in author.books.all()])
Two queries total, regardless of how many authors you have.
3. Fetch Only What You Need
only() and values() stop the ORM from hydrating full model instances when you only need a few columns.
# Loads every column Book.objects.all() # Loads just two columns as dicts — much less memory Book.objects.values("id", "title")
4. Count and Exist Without Loading Rows
# Loads all rows, then measures the list — wasteful if len(Book.objects.filter(active=True)): ... # Pushes the work to the database if Book.objects.filter(active=True).exists(): ...
Use .exists() for presence checks and .count() for totals.
Common Pitfalls
- ▸Calling
.all()inside a template loop re-runs the query. Prefetch first. - ▸
select_relatedon a nullable FK still works but uses a LEFT JOIN — check your query withstr(queryset.query). - ▸Adding
select_related("a", "b", "c")across many tables can produce a giant join. Measure before assuming it is faster.
Summary
Ninety percent of Django ORM performance work is spotting N+1 patterns and reaching for select_related or prefetch_related. Add .only() and .exists() where they fit, and you will rarely need raw SQL at all.