How to make faster queries using Django ORM

If you’re building Django applications at any decent scale, you’ll quickly realize:
Slow queries → slower apps → frustrated users.

The good news?
Django’s ORM is extremely powerful — if you know how to use it right.
In this article, we’ll dive into how to write better, faster Django queries using some simple but highly effective patterns.

We’ll cover:

  • Why .only() matters

  • When and how to use prefetch_related (similarly select_related)

  • How to avoid unnecessary data loading

  • Practical example based on real-world code

Let’s go 🚀.

The Problem: Unoptimized Django ORM Queries

Imagine you have a Django app where users are connected to RSS feed URLs.
Each user can have multiple feeds, but you only want to cache a list of active feed IDs for each user.

Here’s what a basic implementation might look like:

active_users = user.objects.filter(is_active=True).prefetch_related('user_feeds')

for user in active_users:
    feed_ids = [feed.feed_id for feed in user.user_feeds if feed.is_active]
    # cache feed_ids for each user somewhere

Looks fine, right?
Not really.
This query is loading all fields from both User and FeedURL models — even if you only need user_id and feed_id!

It executes something like this:

SELECT * FROM user
WHERE is_active = true;

SELECT * FROM feedurl
INNER JOIN user_feedurl
ON feedurl.id = user_feedurl.feedurl_id;

This loads unnecessary fields which results in

  • Bigger queries sent to the DB

  • More network traffic

  • More RAM usage in your app

  • Slower iteration over results

Optimisations:

  1. Use .only() to Fetch Only What You Need

Always restrict the fields Django fetches using .only(). You never wanna pull all the columns in memory until required.

User.objects.filter(is_active=True).only("user_id")

This will tell Django:

“Only load the user_id field. Ignore everything else."

✅ Faster DB query
✅ Smaller objects in memory
✅ Safer against accidentally touching heavy fields later

2. Prefetch Related Data Smartly When dealing with related models (e.g., user feeds), use prefetch_related — but also control what gets prefetched.

Instead of blindly prefetching all fields:

active_feed_qs = FeedURL.objects.filter(is_active=True).only("feed_id")

active_users = User.objects.filter(is_active=True).only("user_id").prefetch_related(
    Prefetch("user_feeds", queryset=active_feed_qs, to_attr="active_user_feeds")
)

only("feed_id") ensures we don't load unnecessary RSS feed fields
to_attr="active_user_feeds" gives a custom attribute that avoids touching the real user_feeds relation again

Now when looping:

for user in active_users:
    feed_ids = [feed.feed_id for feed in user.active_user_feeds]
    # cache it for each user as required

No extra queries are triggered.
Everything is already loaded and ready. 🏎️

3. Flatten Early, Cache Later

Whenever possible, flatten ORM objects into simple Python lists early — before any heavy processing.

active_feed_ids = [feed.feed_id for feed in user.active_user_feeds]

Then use user.active_feed_ids everywhere instead of touching ORM objects again.

✅ Pure Python list — no ORM overhead
✅ Super fast lookups
✅ Cleaner and safer code

If you don’t need models, for flattening you can also use.values or .values_list while querying

User.objects.filter(is_active=True).values_list("user_id", Flat=True)

This will return a list of values instead of the model object.

User.objects.filter(is_active=True).values("user_id", Flat=True)

This will return dictionary data instead of the model object.

Here’s how an optimized function could look:

def refresh_user_rss_feed_cache():
    active_feed_qs = RSSFeedURL.objects.filter(is_active=True).only("feed_id")
    active_users = User.objects.filter(is_active=True).only("user_id").prefetch_related(
        Prefetch("user_feeds", queryset=active_feed_qs, to_attr="active_user_feeds")
    )

    for user in active_users:
        active_feed_ids = [feed.feed_id for feed in user.active_user_feeds]
        set_cache_value(USER_RSS_FEEDS, active_feed_ids, user.user_id)

✅ Only loads necessary fields
✅ Prefetches minimal related data
✅ No extra queries inside the loop
✅ Clean and efficient

Quick Django ORM Tips 🧠

  • Use .only(): whenever you're sure you need just a few fields.

  • Use .defer(): if you want to skip loading some heavy fields like description or profile_pic.

  • Use select_related: for ForeignKey relationships where you immediately need related data.

  • Use prefetch_related: for ManyToMany and reverse ForeignKey relationships.

  • Flatten early: turn queryset objects into plain Python structures if you just need IDs, names, etc.

Conclusion

Good Django ORM usage is not about being fancy.
It’s about being intentional:
👉 Fetch only what you need.
👉 Prefetch efficiently.
👉 Avoid surprises by flattening early.

These small habits will make your Django apps faster, cheaper to run, and more reliable.

Apart from these use indexing and caching for efficiency.

Happy coding! ⚡

0
Subscribe to my newsletter

Read articles from Prasang Maheshwari directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Prasang Maheshwari
Prasang Maheshwari