惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

人人都是产品经理
人人都是产品经理
有赞技术团队
有赞技术团队
L
LangChain Blog
C
Check Point Blog
博客园 - 【当耐特】
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
V2EX
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
GbyAI
GbyAI
美团技术团队
博客园 - 司徒正美
Google DeepMind News
Google DeepMind News
WordPress大学
WordPress大学
aimingoo的专栏
aimingoo的专栏
S
SegmentFault 最新的问题
A
About on SuperTechFans
Blog — PlanetScale
Blog — PlanetScale
Hugging Face - Blog
Hugging Face - Blog
博客园 - 叶小钗
腾讯CDC
B
Blog
G
Google Developers Blog
The Cloudflare Blog
P
Proofpoint News Feed

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
How I Caught and Fixed an N+1 Query in My Django REST API
Vicente G. R · 2026-05-23 · via DEV Community

Every performant API eventually runs into the same silent killer: the N+1 query problem. It doesn't crash your app. It doesn't throw errors. It just quietly makes every list endpoint slower as your data grows — and it's almost invisible until Sentry flags it in production.

Today, Sentry caught one on my /api/blog-posts/ endpoint. Here's exactly what happened and how I fixed it in three lines of code.


What Is an N+1 Query?

An N+1 query happens when your code fetches a list of N records, then fires an additional query per record to fetch related data — totalling 1 + N database hits instead of a flat 2 or 3.

In Django, this usually happens silently because the ORM is lazy by default. Accessing a related object on a model instance that wasn't eagerly loaded triggers a fresh SELECT on the spot. With 30 blog posts, that's 30 silent queries you never wrote.


The Offending Code

The BlogPostViewSet looked clean on the surface:

class BlogPostViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = BlogPost.objects.all()
    serializer_class = BlogPostSerializer
    lookup_field = "uid"

Enter fullscreen mode Exit fullscreen mode

And the serializer:

class BlogPostSerializer(serializers.ModelSerializer):
    tags = BlogTagSerializer(many=True, read_only=True)
    series = BlogSeriesSerializer(read_only=True)
    ...

Enter fullscreen mode Exit fullscreen mode

Spot the problem? BlogPost has two relations:

  • series — a ForeignKey to BlogSeries
  • tags — a ManyToManyField to BlogTag

When DRF serializes a list of 30 posts, it accesses post.series and post.tags on each one. Without eager loading, Django fires two extra queries per post — one to fetch the series, one to fetch the tags. That's 1 + 60 queries for a 30-post list.

The featured action had the same issue:

@action(detail=False, methods=["get"])
def featured(self, request):
    queryset = BlogPost.objects.filter(date_published__isnull=False).order_by(
        "-date_published",
    )[:3]

Enter fullscreen mode Exit fullscreen mode

A fresh BlogPost.objects call with no eager loading.


The Fix

Django gives me two tools for this:

  • select_related() — for ForeignKey and OneToOne relations. Issues a SQL JOIN and fetches everything in a single query.
  • prefetch_related() — for ManyToMany and reverse FK relations. Issues a second query and caches the results in Python.

The fix:

class BlogPostViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = BlogPost.objects.select_related("series").prefetch_related("tags")
    serializer_class = BlogPostSerializer
    lookup_field = "uid"

    @action(detail=False, methods=["get"])
    def featured(self, request):
        queryset = (
            BlogPost.objects.select_related("series")
            .prefetch_related("tags")
            .filter(date_published__isnull=False)
            .order_by("-date_published")[:3]
        )
        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)

Enter fullscreen mode Exit fullscreen mode

With 30 posts, the list endpoint now costs 3 queries regardless of dataset size:

  1. SELECT * FROM core_blogpost ...
  2. SELECT * FROM core_blogseries WHERE id IN (...)
  3. SELECT * FROM core_blogtag INNER JOIN core_blogpost_tags WHERE blogpost_id IN (...)

The Bonus Fix

While auditing the blog endpoint, I spotted the same pattern in TestimonialViewSet. Its serializer accesses project.title and project.slug, but the queryset had no select_related:

# Before
queryset = Testimonial.objects.all()

# After
queryset = Testimonial.objects.select_related("project")

Enter fullscreen mode Exit fullscreen mode

One extra line, one less N+1.


How to Spot This in Your Own Code

The pattern is always the same — look for any ViewSet or view where:

  1. The queryset has no select_related or prefetch_related
  2. The serializer accesses a related field (source="relation.field", nested serializers, SerializerMethodField that touches obj.relation)

Tools that help catch this before Sentry does:

  • django-debug-toolbar — shows query counts per request in the browser
  • nplusone — raises exceptions in tests when N+1 queries are detected
  • Sentry Performance — catches it in production with query traces

The best time to catch an N+1 is during code review. Any time you write a nested serializer, ask: does the queryset for this view eagerly load this relation?


Takeaway

The Django ORM's lazy evaluation is a feature, not a bug — but it requires discipline at the queryset layer. A clean-looking viewset with objects.all() is often hiding a query storm one serializer away.

The rule of thumb: every relation accessed in a serializer needs a corresponding select_related or prefetch_related on the queryset. Make it a checklist item on every PR that touches a ViewSet.