← Назад к вопросам
Как решена проблема 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)
Правило большого пальца
- select_related() — для ForeignKey и OneToOne (JOIN)
- prefetch_related() — для ManyToMany и обратные отношения (два запроса)
- only() — когда нужны только некоторые поля
- annotate() — когда нужно агрегировать данные
- 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)
Ключевая идея: всегда знай, сколько запросов выполняет твой код!