← Назад к вопросам

Как решена проблема N+1 в Django?

2.0 Middle🔥 211 комментариев
#Django#Базы данных (SQL)

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Проблема N+1 в Django

Что это?

Проблема N+1 — это когда вместо одного запроса к БД выполняется 1 + N запросов. Например:

  • 1 запрос: получить список всех авторов
  • N запросов: для каждого автора получить его статьи

Итого: 1 + N запросов вместо идеального 1-2 запросов.

Пример проблемы

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField()

class Article(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    content = models.TextField()

# Проблема N+1
autors = Author.objects.all()  # 1 запрос

for author in autors:  # Для каждого автора
    articles = author.article_set.all()  # N запросов
    print(f"{author.name}: {articles.count()} articles")

# SQL выполнится:
# SELECT * FROM authors;  -- 1 запрос
# SELECT * FROM articles WHERE author_id = 1;  -- Запрос 1
# SELECT * FROM articles WHERE author_id = 2;  -- Запрос 2
# SELECT * FROM articles WHERE author_id = 3;  -- Запрос 3
# ...
# SELECT * FROM articles WHERE author_id = N;  -- Запрос N

# Всего: 1 + N запросов

Решение 1: select_related()

Для отношений ForeignKey и OneToOne используем select_related() — выполняет LEFT JOIN.

# Оптимизированный запрос
articles = Article.objects.select_related("author").all()

for article in articles:
    print(f"{article.title} by {article.author.name}")

# SQL выполнится:
# SELECT articles.*, authors.* 
# FROM articles 
# LEFT JOIN authors ON articles.author_id = authors.id;

# Всего: 1 запрос

Примеры select_related:

# Один уровень
Articles = Article.objects.select_related("author")

# Несколько уровней (с точкой)
articles = Article.objects.select_related("author", "category")

# Вложенные отношения
articles = Article.objects.select_related("author", "author__publisher")

# С фильтром
articles = Article.objects.select_related("author").filter(published=True)

Решение 2: prefetch_related()

Для отношений ManyToMany и обратных ForeignKey используем prefetch_related().

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

class Article(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

# Проблема N+1
authors = Author.objects.all()  # 1 запрос

for author in authors:
    articles = author.article_set.all()  # N запросов
    print(f"{author.name}: {articles.count()}")

# Решение: prefetch_related()
authors = Author.objects.prefetch_related("article_set").all()

for author in authors:
    articles = author.article_set.all()  # БД не запрашивается!
    print(f"{author.name}: {articles.count()}")

# SQL выполнится:
# SELECT * FROM authors;  -- 1 запрос
# SELECT * FROM articles WHERE author_id IN (1, 2, 3, ...);  -- 1 запрос

# Всего: 2 запроса

Примеры prefetch_related:

# Обратные отношения
authors = Author.objects.prefetch_related("article_set")

# ManyToMany
tags = Tag.objects.prefetch_related("articles")

# Вложенные prefetch
authors = Author.objects.prefetch_related("article_set").all()

articles = Article.objects.prefetch_related("tags").all()

Решение 3: only() и defer()

Уменьшаем количество данных в одном запросе.

# Получаем только нужные поля
articles = Article.objects.select_related("author").only(
    "id", "title", "author__name"
)

# Исключаем тяжелые поля
articles = Article.objects.defer("content")  # Не загружаем контент

for article in articles:
    print(article.title)  # Из кэша
    print(article.content)  # Новый запрос!

Решение 4: Annotate и Aggregate

Вычисляем на уровне БД, а не Python.

from django.db.models import Count, Q

# Плохо - считаем в Python
authors = Author.objects.all()
for author in authors:
    article_count = author.article_set.count()  # N+1 запросов
    print(f"{author.name}: {article_count}")

# Хорошо - считаем в БД
from django.db.models import Count

authors = Author.objects.annotate(
    article_count=Count("article")
).all()

for author in authors:
    print(f"{author.name}: {author.article_count}")  # Из кэша

# SQL выполнится:
# SELECT authors.*, COUNT(articles.id) as article_count
# FROM authors
# LEFT JOIN articles ON authors.id = articles.author_id
# GROUP BY authors.id;

# Всего: 1 запрос

Примеры annotate:

# Добавляем поле в результат
authors = Author.objects.annotate(
    article_count=Count("article"),
    published_count=Count("article", filter=Q(article__published=True))
)

# Сумма, среднее, минимум
from django.db.models import Sum, Avg, Min, Max

articles = Article.objects.annotate(
    total_comments=Count("comments"),
    avg_rating=Avg("rating"),
    min_date=Min("created_at")
)

Решение 5: Чтение только необходимых полей

# Получаем только 2 поля
articles = Article.objects.only("id", "title").all()

# Или через values, values_list
articles = Article.objects.values("id", "title").all()
# Возвращает список словарей

articles = Article.objects.values_list("title", flat=True).all()
# Возвращает плоский список

Практический пример с API

from rest_framework import serializers
from rest_framework.views import APIView
from rest_framework.response import Response

class AuthorSerializer(serializers.ModelSerializer):
    class Meta:
        model = Author
        fields = ["id", "name"]

class ArticleSerializer(serializers.ModelSerializer):
    author = AuthorSerializer(read_only=True)
    
    class Meta:
        model = Article
        fields = ["id", "title", "author"]

class ArticleListView(APIView):
    def get(self, request):
        # Оптимизированный запрос
        articles = Article.objects.select_related("author").filter(
            published=True
        )
        
        serializer = ArticleSerializer(articles, many=True)
        return Response(serializer.data)

Правило большого пальца

  1. select_related() — для ForeignKey и OneToOne (JOIN)
  2. prefetch_related() — для ManyToMany и обратные отношения (два запроса)
  3. only() — когда нужны только некоторые поля
  4. annotate() — когда нужно агрегировать данные
  5. values() — когда нужны только данные без объектов

Отладка N+1

from django.db import connection
from django.test.utils import override_settings

# В settings для разработки
LOGGING = {
    "version": 1,
    "handlers": {
        "console": {"class": "logging.StreamHandler"},
    },
    "loggers": {
        "django.db.backends": {
            "handlers": ["console"],
            "level": "DEBUG",
        },
    },
}

# Или используем django-debug-toolbar
# pip install django-debug-toolbar

# Вручную проверить количество запросов
from django.test import TestCase

class ArticleTestCase(TestCase):
    def test_article_list(self):
        from django.test.utils import override_settings
        with self.assertNumQueries(2):  # Ожидаем ровно 2 запроса
            articles = Article.objects.select_related("author").all()
            list(articles)  # Выполняем запрос
            for article in articles:
                print(article.author.name)

Ключевая идея: всегда знай, сколько запросов выполняет твой код!

Как решена проблема N+1 в Django? | PrepBro