---
# === IDENTITY ===
id: software/debugging/django-n-plus-1/2026
canonical_question: "How do I detect and fix N+1 query problems in Django?"
aliases:
  - "Django N+1 query"
  - "Django too many queries"
  - "Django select_related"
  - "Django prefetch_related"
  - "Django ORM slow queries"
  - "Django lazy loading performance"
  - "Django database query optimization"
  - "Django Prefetch object"
  - "Django auto-prefetch"
  - "Django query count middleware"
entity_type: software_reference
domain: software > debugging > django_n_plus_1
region: global
jurisdiction: global
temporal_scope: 2020-2026

# === VERIFICATION ===
last_verified: 2026-05-17
confidence: 0.94
version: 2.1
first_published: 2026-02-17

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "Django 6.0 (2025-12) — get_prefetch_queryset() and Prefetch.get_current_queryset() removed; DEFAULT_AUTO_FIELD now BigAutoField"
  next_review: 2026-11-13
  change_sensitivity: low

# === CONSTRAINTS ===
constraints:
  - "select_related() works ONLY with ForeignKey and OneToOneField — using it on ManyToMany or reverse FK silently falls back to lazy loading"
  - "Calling .filter(), .all(), or .exclude() on a prefetched relationship creates a new queryset and re-queries the database, ignoring the prefetch cache"
  - "Never call select_related() without explicit field names in production — the bare form follows ALL FK chains and can produce massive JOINs"
  - "Django 4.2+ async ORM variants (aselect_related, aprefetch_related) are awaitable and must not be mixed with synchronous code"
  - "Prefetch() with to_attr returns a Python list, not a QuerySet — calling .filter() on the result raises AttributeError"
  - "assertNumQueries() counts vary by Django version, database backend, and middleware — always calibrate expected counts for your specific stack"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Slow queries caused by missing database indexes, not relationship loading"
    use_instead: "software/debugging/django-slow-queries/2026"
  - condition: "Performance issue is in raw SQL or custom Manager, not ORM lazy loading"
    use_instead: "software/debugging/django-raw-sql-optimization/2026"
  - condition: "Using SQLAlchemy instead of Django ORM"
    use_instead: "software/debugging/sqlalchemy-n-plus-1/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: relationship_type
    question: "What type of Django model relationship is causing the extra queries?"
    type: choice
    options: ["ForeignKey/OneToOne", "ManyToMany", "Reverse ForeignKey", "Not sure"]
  - key: context
    question: "Where does the N+1 query occur?"
    type: choice
    options: ["DRF serializer", "Template", "Django Admin", "Management command", "View/queryset"]
  - key: django_version
    question: "Which Django version are you using?"
    type: choice
    options: ["Django 6.0+", "Django 5.x", "Django 4.2 LTS", "Django 3.2 or older"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/debugging/django-n-plus-1/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-05-17)"

# === RELATED UNITS ===
related_kos:
  related_to:
    - id: "software/debugging/python-memory-leaks/2026"
      label: "Python Memory Leaks"
    - id: "software/debugging/python-importerror/2026"
      label: "Python ImportError/ModuleNotFoundError"
    - id: "software/debugging/postgresql-slow-queries/2026"
      label: "PostgreSQL Slow Queries"
  often_confused_with:
    - id: "software/debugging/graphql-n-plus-1-dataloader/2026"
      label: "GraphQL N+1 (DataLoader pattern)"

# === SOURCES ===
sources:
  - id: src1
    title: "Django — select_related and prefetch_related"
    author: Django Software Foundation
    url: https://docs.djangoproject.com/en/5.2/ref/models/querysets/#select-related
    type: official_docs
    published: 2025-04-02
    reliability: authoritative
  - id: src2
    title: "Django — Prefetch object"
    author: Django Software Foundation
    url: https://docs.djangoproject.com/en/5.2/ref/models/querysets/#prefetch-objects
    type: official_docs
    published: 2025-04-02
    reliability: authoritative
  - id: src3
    title: "Django — Database optimization"
    author: Django Software Foundation
    url: https://docs.djangoproject.com/en/5.2/topics/db/optimization/
    type: official_docs
    published: 2025-04-02
    reliability: authoritative
  - id: src4
    title: "Sentry — N+1 Query Detection in Django"
    author: Sentry
    url: https://docs.sentry.io/product/issues/issue-details/performance-issues/n-one-queries/
    type: technical_blog
    published: 2025-01-01
    reliability: high
  - id: src5
    title: "Scout APM — Finding N+1 Queries in Django"
    author: Scout APM
    url: https://scoutapm.com/blog/django-n-1-queries
    type: technical_blog
    published: 2024-06-01
    reliability: high
  - id: src6
    title: "django-debug-toolbar"
    author: Django Debug Toolbar contributors
    url: https://django-debug-toolbar.readthedocs.io/
    type: official_docs
    published: 2025-01-01
    reliability: high
  - id: src7
    title: "nplusone — Auto-detecting N+1 in Django"
    author: Joshua Carp
    url: https://github.com/jmcarp/nplusone
    type: community_resource
    published: 2024-01-01
    reliability: moderate_high
  - id: src8
    title: "Haki Benita — Django ORM Optimization Tips"
    author: Haki Benita
    url: https://hakibenita.com/all-you-need-to-know-about-prefetching-in-django
    type: technical_blog
    published: 2024-06-01
    reliability: high
  - id: src9
    title: "django-auto-prefetch — Automatic FK prefetching"
    author: Adam Johnson
    url: https://pypi.org/project/django-auto-prefetch/
    type: community_resource
    published: 2025-09-18
    reliability: high
  - id: src10
    title: "django-zen-queries — Explicit query control"
    author: DabApps
    url: https://github.com/dabapps/django-zen-queries
    type: community_resource
    published: 2024-01-01
    reliability: moderate_high
---

# How to Detect and Fix N+1 Query Problems in Django

## TL;DR

- **Bottom line**: An N+1 query happens when your code executes 1 query to fetch N objects, then N additional queries to fetch related data for each — resulting in N+1 total queries instead of 2. In Django, this occurs because the ORM lazy-loads related objects by default. Fix with `select_related()` (ForeignKey/OneToOne — SQL JOIN) or `prefetch_related()` (ManyToMany/reverse FK — separate query + Python join).
- **Key tool/command**: `django-debug-toolbar` shows all SQL queries per request with timing. For automated detection: `nplusone` library logs warnings when N+1 patterns are detected. For zero-config fix: `django-auto-prefetch` automatically prefetches ForeignKey values as needed. For tests: `self.assertNumQueries(N)`.
- **Watch out for**: Serializers (DRF) and templates that access related fields — they trigger lazy loads invisibly. A template loop like `{% for book in books %}{{ book.author.name }}{% endfor %}` fires N author queries. Also: calling `.filter()` on a prefetched relation invalidates the cache.
- **Works with**: Django 2.0+ (all features stable). Django 5.2 LTS and 6.0 recommended for latest ORM optimizations including async prefetch support. **Django 6.0 breaking change**: `get_prefetch_queryset()` and `Prefetch.get_current_queryset()` were removed — custom prefetcher subclasses must use `get_prefetcher()` / `prefetch_related_objects()` instead. Requires Python 3.12+. [src1]

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- `select_related()` works ONLY with ForeignKey and OneToOneField — using it on ManyToMany or reverse FK silently falls back to lazy loading [src1]
- Calling `.filter()`, `.all()`, or `.exclude()` on a prefetched relationship creates a new queryset and re-queries the database, ignoring the prefetch cache [src2, src8]
- Never call `select_related()` without explicit field names in production — the bare form follows ALL FK chains and can produce massive JOINs [src1, src3]
- Django 4.2+ async ORM variants (`aselect_related`, `aprefetch_related`) are awaitable and must not be mixed with synchronous code [src1]
- `Prefetch()` with `to_attr` returns a Python list, not a QuerySet — calling `.filter()` on the result raises `AttributeError` [src2]
- `assertNumQueries()` counts vary by Django version, database backend, and middleware — always calibrate expected counts for your specific stack [src3]

## Quick Reference

| # | Pattern | Queries | Fix | Result |
|---|---|---|---|---|
| 1 | Loop accessing ForeignKey | 1 + N | `select_related('fk_field')` | 1 query (SQL JOIN) [src1] |
| 2 | Loop accessing reverse FK | 1 + N | `prefetch_related('related_set')` | 2 queries [src1] |
| 3 | Loop accessing ManyToMany | 1 + N | `prefetch_related('m2m_field')` | 2 queries [src1] |
| 4 | Nested relationship (FK -> FK) | 1 + N + N^2 | `select_related('fk__nested_fk')` | 1 query [src1] |
| 5 | FK + filtered reverse set | 1 + N | `Prefetch('related_set', queryset=...)` | 2 queries [src2] |
| 6 | Aggregate per related object | 1 + N | `annotate(count=Count('related'))` | 1 query [src3] |
| 7 | Serializer accessing FK | 1 + N | Override `get_queryset()` with `select_related` | 1-2 queries [src1, src4] |
| 8 | Template accessing FK in loop | 1 + N | Pass optimized queryset to template context | 1-2 queries [src1, src5] |
| 9 | Admin list_display with FK | 1 + N | Override `get_queryset()` in ModelAdmin | 1-2 queries [src3] |
| 10 | Bulk create/update in loop | N inserts/updates | `bulk_create()` / `bulk_update()` | 1 query [src3] |
| 11 | Generic relation access | 1 + N | `prefetch_related()` with `GenericPrefetch` (Django 4.2+) | 2 queries [src2] |
| 12 | Entire model with many FKs | 1 + N per FK | `django-auto-prefetch` model base class | 2 queries per FK (automatic) [src9] |

## Decision Tree

```
START
├── What relationship type causes the extra queries?
│   ├── ForeignKey or OneToOneField (forward) → select_related() [src1]
│   ├── ManyToManyField → prefetch_related() [src1]
│   ├── Reverse ForeignKey (related_set) → prefetch_related() [src1]
│   └── Generic relation → prefetch_related() with GenericPrefetch [src2]
├── Do you need to filter/order the related objects?
│   ├── YES → Use Prefetch() object with custom queryset [src2]
│   └── NO → Use plain select_related() or prefetch_related()
├── Do you need an aggregate (count, sum, avg)?
│   ├── YES → Use annotate() instead of fetching objects [src3]
│   └── NO ↓
├── Is it in DRF serializer?
│   ├── YES → Override get_queryset() in ViewSet/APIView [src4]
│   └── NO ↓
├── Is it in a template?
│   ├── YES → Optimize queryset in view before passing to context [src5]
│   └── NO ↓
├── Is it in Django admin?
│   ├── YES → Override get_queryset() in ModelAdmin, add list_select_related [src3]
│   └── NO ↓
├── Is it bulk create/update?
│   ├── YES → Use bulk_create() / bulk_update() [src3]
│   └── NO ↓
├── Want zero-config automatic prefetching?
│   ├── YES → Use django-auto-prefetch model base class [src9]
│   └── NO ↓
├── Want to prevent any lazy queries in specific code blocks?
│   ├── YES → Use django-zen-queries context manager [src10]
│   └── NO ↓
└── Profile with django-debug-toolbar or nplusone [src6, src7]
```

## Step-by-Step Guide

### 1. Detect the N+1 problem

Install detection tools to find N+1 queries before they hit production. [src5, src6, src7]

```bash
# Install django-debug-toolbar (development)
pip install django-debug-toolbar

# Install nplusone (auto-detection)
pip install nplusone

# Install django-auto-prefetch (zero-config prevention)
pip install django-auto-prefetch

# Install django-zen-queries (explicit query control)
pip install django-zen-queries
```

```python
# settings.py — Enable debug toolbar
INSTALLED_APPS = [
    ...
    'debug_toolbar',
]
MIDDLEWARE = [
    'debug_toolbar.middleware.DebugToolbarMiddleware',
    ...
]
INTERNAL_IPS = ['127.0.0.1']

# settings.py — Enable nplusone
INSTALLED_APPS = [
    ...
    'nplusone.ext.django',
]
MIDDLEWARE = [
    'nplusone.ext.django.NPlusOneMiddleware',
    ...
]
NPLUSONE_RAISE = True  # Raise exceptions in development
```

**Verify**: Open django-debug-toolbar SQL panel — if you see N similar queries that differ only by ID, you have an N+1 problem.

### 2. Use select_related() for ForeignKey / OneToOne

`select_related()` performs a SQL JOIN to fetch related objects in the same query. [src1]

```python
# Models
class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

# BAD — N+1: 1 query for books + N queries for authors
books = Book.objects.all()
for book in books:
    print(book.author.name)  # Each access triggers a query!

# GOOD — FIX: 1 query total (SQL JOIN)
books = Book.objects.select_related('author').all()
for book in books:
    print(book.author.name)  # No extra query — already loaded

# Chain for nested relationships
books = Book.objects.select_related('author__publisher').all()
```

**Verify**: Check SQL panel — should show 1 query with JOIN instead of N+1 queries.

### 3. Use prefetch_related() for ManyToMany / reverse FK

`prefetch_related()` executes a separate query and joins in Python. [src1]

```python
# Models
class Tag(models.Model):
    name = models.CharField(max_length=50)

class Book(models.Model):
    title = models.CharField(max_length=200)
    tags = models.ManyToManyField(Tag)

# BAD — N+1: 1 query for books + N queries for tags
books = Book.objects.all()
for book in books:
    print(book.tags.all())  # Each triggers a query!

# GOOD — FIX: 2 queries total
books = Book.objects.prefetch_related('tags').all()
for book in books:
    print(book.tags.all())  # Already prefetched

# Reverse FK (from Author side)
authors = Author.objects.prefetch_related('book_set').all()
for author in authors:
    print(author.book_set.all())  # 2 queries total
```

**Verify**: Debug toolbar shows exactly 2 queries instead of N+1.

### 4. Use Prefetch() for filtered/custom querysets

The `Prefetch` object allows filtering, ordering, or annotating prefetched items. [src2, src8]

```python
from django.db.models import Prefetch

# Prefetch only published books, ordered by date
authors = Author.objects.prefetch_related(
    Prefetch(
        'book_set',
        queryset=Book.objects.filter(
            published=True
        ).order_by('-pub_date')[:5],
        to_attr='recent_books'  # Access via author.recent_books
    )
)

for author in authors:
    for book in author.recent_books:  # List, not QuerySet
        print(book.title)

# Nested prefetch
publishers = Publisher.objects.prefetch_related(
    Prefetch(
        'author_set',
        queryset=Author.objects.prefetch_related(
            Prefetch('book_set', queryset=Book.objects.filter(published=True))
        )
    )
)
```

**Verify**: 2-3 queries total regardless of data size.

### 5. Use annotate() for aggregates

Don't fetch all related objects just to count them — use database aggregation. [src3]

```python
from django.db.models import Count, Avg, Sum, Q

# BAD — N+1: Fetches all books just to count them
authors = Author.objects.all()
for author in authors:
    count = author.book_set.count()  # N extra queries!

# GOOD — FIX: 1 query with annotation
authors = Author.objects.annotate(
    book_count=Count('book'),
    published_count=Count('book', filter=Q(book__published=True)),
    avg_rating=Avg('book__rating'),
)
for author in authors:
    print(f"{author.name}: {author.book_count} books, avg {author.avg_rating}")
```

**Verify**: 1 query with GROUP BY instead of N+1.

### 6. Fix N+1 in Django REST Framework

DRF serializers are a very common source of N+1 queries. [src4]

```python
# serializers.py
class BookSerializer(serializers.ModelSerializer):
    author_name = serializers.CharField(source='author.name')

    class Meta:
        model = Book
        fields = ['id', 'title', 'author_name']

# views.py — BAD: N+1 (default queryset, no optimization)
class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

# views.py — GOOD: Override get_queryset
class BookViewSet(viewsets.ModelViewSet):
    serializer_class = BookSerializer

    def get_queryset(self):
        return Book.objects.select_related('author').all()
```

**Verify**: API response triggers 1 query instead of N+1.

### 7. Fix N+1 in Django Admin

Admin list views commonly trigger N+1 queries. [src3]

```python
# admin.py
class BookAdmin(admin.ModelAdmin):
    list_display = ['title', 'author_name', 'publisher_name']
    list_select_related = ['author', 'publisher']  # Django 1.11+

    def author_name(self, obj):
        return obj.author.name

    def publisher_name(self, obj):
        return obj.publisher.name

    # Alternative: override get_queryset
    def get_queryset(self, request):
        return super().get_queryset(request).select_related(
            'author', 'publisher'
        ).prefetch_related('tags')
```

**Verify**: Admin list page uses 1-2 queries regardless of page size.

### 8. Use django-auto-prefetch for zero-config prevention

`django-auto-prefetch` automatically detects when a ForeignKey or OneToOneField is accessed in a loop and speculatively prefetches it for all instances in the same queryset. [src9]

```python
# models.py — Replace standard imports with auto_prefetch
import auto_prefetch

class Author(auto_prefetch.Model):
    name = models.CharField(max_length=100)

    class Meta(auto_prefetch.Model.Meta):
        pass

class Book(auto_prefetch.Model):
    title = models.CharField(max_length=200)
    author = auto_prefetch.ForeignKey(Author, on_delete=models.CASCADE)

    class Meta(auto_prefetch.Model.Meta):
        pass

# Now this code automatically prefetches — no select_related needed
books = Book.objects.all()
for book in books:
    print(book.author.name)  # Auto-prefetched on first access
```

**Verify**: `django-auto-prefetch` supports Django 4.2 through 6.0 and Python 3.9 through 3.14. Latest release: v1.14.0 (2025-09-18).

## Code Examples

### Comprehensive queryset optimizer

> Full script: [comprehensive-queryset-optimizer.py](scripts/comprehensive-queryset-optimizer.py) (68 lines)

```python
# Input:  Views with inconsistent query optimization
# Output: Reusable queryset manager that ensures optimal loading
from django.db import models
from django.db.models import Count, Prefetch
class BookQuerySet(models.QuerySet):
# ... (see full script)
```

### assertNumQueries test helper

> Full script: [assertnumqueries-test-helper.py](scripts/assertnumqueries-test-helper.py) (36 lines)

```python
# Input:  Need to catch N+1 regressions in tests
# Output: Test patterns that enforce query count limits
from django.test import TestCase
class BookAPITest(TestCase):
    def setUp(self):
# ... (see full script)
```

### Query count middleware for development

> Full script: [query-count-middleware-for-development.py](scripts/query-count-middleware-for-development.py) (41 lines)

```python
# Input:  Want to detect N+1 queries in development without debug toolbar
# Output: Middleware that logs warning when too many queries run
import logging
from django.db import connection, reset_queries
from django.conf import settings
# ... (see full script)
```

## Anti-Patterns

### Wrong: Accessing ForeignKey in a loop

```python
# BAD — N+1 queries: 1 for books + N for authors [src1, src5]
books = Book.objects.all()
for book in books:
    print(f"{book.title} by {book.author.name}")  # N extra queries!
```

### Correct: select_related before the loop

```python
# GOOD — 1 query with SQL JOIN [src1]
books = Book.objects.select_related('author').all()
for book in books:
    print(f"{book.title} by {book.author.name}")  # Already loaded
```

### Wrong: Counting related objects in a loop

```python
# BAD — N+1: 1 for authors + N count queries [src3]
authors = Author.objects.all()
data = [
    {'name': a.name, 'books': a.book_set.count()}
    for a in authors
]
```

### Correct: annotate() aggregates in one query

```python
# GOOD — 1 query with COUNT annotation [src3]
from django.db.models import Count

authors = Author.objects.annotate(book_count=Count('book'))
data = [
    {'name': a.name, 'books': a.book_count}
    for a in authors
]
```

### Wrong: DRF serializer without queryset optimization

```python
# BAD — Serializer accesses FK fields, causing N+1 [src4]
class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()  # No select_related!
    serializer_class = BookSerializer  # Accesses book.author.name
```

### Correct: Override get_queryset with eager loading

```python
# GOOD — Eager-load everything the serializer needs [src4]
class BookViewSet(viewsets.ModelViewSet):
    serializer_class = BookSerializer

    def get_queryset(self):
        return Book.objects.select_related(
            'author', 'publisher'
        ).prefetch_related('tags')
```

### Wrong: Filtering a prefetched relationship

```python
# BAD — .filter() on prefetched set re-queries the database [src2, src8]
books = Book.objects.prefetch_related('tags').all()
for book in books:
    # This ignores the prefetch cache and hits the DB!
    python_tags = book.tags.filter(name__startswith='python')
```

### Correct: Use Prefetch() with to_attr for filtered access

```python
# GOOD — Filter inside Prefetch, access via to_attr [src2, src8]
from django.db.models import Prefetch

books = Book.objects.prefetch_related(
    Prefetch(
        'tags',
        queryset=Tag.objects.filter(name__startswith='python'),
        to_attr='python_tags'
    )
)
for book in books:
    print(book.python_tags)  # List, already filtered, no extra query
```

## Common Pitfalls

- **`select_related` vs `prefetch_related` confusion**: `select_related` works only for ForeignKey/OneToOne (creates SQL JOIN). `prefetch_related` works for ManyToMany and reverse ForeignKey (separate query + Python join). Using the wrong one fails silently. [src1]
- **`prefetch_related` invalidated by `.filter()`**: Calling `.filter()` on a prefetched queryset triggers a new query, defeating the prefetch. Use `Prefetch()` object with a filtered queryset and `to_attr` instead. [src2, src8]
- **Template lazy loading**: Django templates access related objects lazily — `{{ book.author.name }}` triggers a query even though it doesn't look like Python code. Always pass optimized querysets to templates. [src5]
- **DRF serializer depth**: Setting `depth = 1` on a ModelSerializer causes it to serialize related objects, triggering lazy loads for each row. Override `get_queryset()` to match. [src4]
- **`only()` breaking `select_related`**: If you use `only()` to limit fields but forget the FK field, Django can't follow the relationship and falls back to lazy loading. Always include FK columns in `only()`. [src3]
- **Ordering by related field**: `Book.objects.order_by('author__name')` doesn't join — it triggers a separate query. Combine with `select_related('author')`. [src1]
- **`select_related` with `values()`/`values_list()`**: Using `select_related()` alongside `values()` is pointless — `values()` returns dicts, not model instances, so the joined data is discarded. Use `values('author__name')` directly instead. [src3]
- **Prefetch cache not shared across querysets**: Each queryset maintains its own prefetch cache. Cloning a queryset (e.g., slicing, re-filtering) creates a new cache. Prefetch once and iterate. [src2]

## Diagnostic Commands

```python
# Django shell: count queries for a code block
from django.db import connection, reset_queries
from django.conf import settings

settings.DEBUG = True
reset_queries()

# ... your code here ...
books = Book.objects.all()
for book in books:
    _ = book.author.name

print(f"Total queries: {len(connection.queries)}")
for q in connection.queries:
    print(f"  [{q['time']}s] {q['sql'][:120]}")
```

```bash
# Enable SQL logging in settings.py
# settings.py
LOGGING = {
    'version': 1,
    'handlers': {'console': {'class': 'logging.StreamHandler'}},
    'loggers': {
        'django.db.backends': {
            'level': 'DEBUG',
            'handlers': ['console'],
        }
    }
}

# Install and use nplusone
pip install nplusone
# Add to INSTALLED_APPS and MIDDLEWARE (see step 1)
# It will log: "Potential n+1 query detected on `Book.author`"

# Install django-debug-toolbar
pip install django-debug-toolbar
# SQL panel shows all queries with timing and EXPLAIN

# Use django-zen-queries to enforce no-query zones
pip install django-zen-queries
# In views: with queries_disabled(): render(request, template, context)
```

## Version History & Compatibility

| Version | Behavior | Key Changes |
|---|---|---|
| Django 6.0 (2025-12) | Stable (Python 3.12+ required) | `get_prefetch_queryset()` and `Prefetch.get_current_queryset()` REMOVED — custom prefetcher subclasses must migrate to `get_prefetcher()` / `prefetch_related_objects()`. `StringAgg` aggregate on all backends; `BigAutoField` default; `AnyValue` aggregate added [src1] |
| Django 5.2 LTS (2025-04) | Stable (recommended) | `values()`/`values_list()` ordering now matches specified order; `prefetch_related` bug fixes for composite PKs [src1] |
| Django 5.1 (2024-08) | Stable | Composite primary keys; `bulk_create()` used in admin delete action [src1] |
| Django 5.0 (2023-12) | Stable | `GeneratedField`; improved query compilation [src1] |
| Django 4.2 LTS (2023-04) | Stable | Async ORM: `aselect_related()`, `aprefetch_related()` [src1] |
| Django 4.1 (2022-08) | `Prefetch` improvements | Async ORM methods; better Prefetch debugging [src2] |
| Django 3.2 LTS (2021-04) | Stable | `prefetch_related` performance improvements [src1] |
| Django 2.0 (2017-12) | Prefetch object | `Prefetch()` object for custom querysets [src2] |
| Django 1.11 (2017-04) | `list_select_related` | Admin `list_select_related` attribute [src3] |
| Django 1.4 (2012-03) | `prefetch_related` | ManyToMany eager loading support [src1] |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Loop accessing related FK objects | Single object detail view (1 related) | Just access the FK normally |
| Template displays list with related data | Aggregating, not displaying individual objects | `annotate()` + `aggregate()` |
| DRF serializer includes nested objects | Related data is optional / rarely needed | Conditional prefetching |
| Admin list shows FK columns | QuerySet is already filtered to 1 result | No optimization needed |
| Management command iterating large queryset | Read-heavy endpoint already using `values()` | `values('fk__field')` directly |

## Important Caveats

- `select_related()` uses SQL JOINs which can produce large result sets for deeply nested relationships. For each additional JOIN, the result set multiplies. Limit depth with `select_related('author')` rather than `select_related()` (all FK fields).
- `prefetch_related()` does the joining in Python. For very large related sets (millions of rows), this can cause high memory usage. Use `Prefetch()` with `.filter()` or slicing to limit the prefetched set.
- Calling `.all()`, `.filter()`, or `.exclude()` on a prefetched relationship creates a new queryset and hits the database again, ignoring the prefetch cache. Access prefetched data via iteration or `to_attr`.
- `assertNumQueries()` counts vary with Django version, database backend, and middleware. Set expected counts based on your specific setup, not theoretical minimums.
- Django 4.2+ provides async variants (`aselect_related`, `aprefetch_related`) for use in async views and middleware, but they are awaitable and cannot be mixed with sync code.
- `django-auto-prefetch` is a third-party package (not part of Django core). It modifies model inheritance and may conflict with custom managers or querysets. Test thoroughly before adopting in existing projects. [src9]
- OpenTelemetry and Sentry both offer N+1 detection in production. Consider enabling performance monitoring if debug toolbar and nplusone are insufficient for catching production-only N+1 issues. [src4]

## Related Units
<!-- Generated from related_kos frontmatter -->

- [Python Memory Leaks](/software/debugging/python-memory-leaks/2026)
- [Python ImportError/ModuleNotFoundError](/software/debugging/python-importerror/2026)
- [PostgreSQL Slow Queries](/software/debugging/postgresql-slow-queries/2026)
- [GraphQL N+1 (DataLoader pattern)](/software/debugging/graphql-n-plus-1-dataloader/2026)
