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

Как решается проблема N+1 запросов?

1.7 Middle🔥 201 комментариев
#Архитектура и паттерны#Базы данных (SQL)

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

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

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

Проблема N+1 запросов и её решение

Проблема N+1 — это классическая проблема производительности, когда приложение делает один запрос для получения N объектов, а потом ещё N запросов для получения связанных данных. Итого: 1 + N запросов.

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

Плохой код (N+1 проблема):

# Запрос 1: получить всех авторов
authors = Author.objects.all()  # SELECT * FROM authors

# Запросы 2-N: для каждого автора получить его статьи
for author in authors:
    articles = author.articles.all()  # SELECT * FROM articles WHERE author_id = X
    print(f"{author.name}: {len(articles)} articles")

# Если авторов 100, будет 101 запрос:
# 1 основной + 100 для каждого автора

В SQL это выглядит так:

-- Запрос 1
SELECT * FROM authors;  -- 100 результатов

-- Запросы 2-101 (по одному для каждого автора)
SELECT * FROM articles WHERE author_id = 1;
SELECT * FROM articles WHERE author_id = 2;
SELECT * FROM articles WHERE author_id = 3;
... (100 раз)

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

select_related() используется для получения связанного объекта в одном запросе через INNER JOIN:

# Хорошо: 1 запрос вместо 101
articles = Article.objects.select_related('author').all()

for article in articles:
    print(f"{article.title} by {article.author.name}")
    # article.author уже загруженный из первого запроса

SQL запрос:

SELECT articles.*, authors.*
FROM articles
INNER JOIN authors ON articles.author_id = authors.id;

Когда использовать:

  • ForeignKey связи
  • OneToOne связи
  • Связи один-к-одному

Решение 2: prefetch_related (ManyToMany и reverse ForeignKey)

prefetch_related() делает отдельные запросы, но загружает все данные за раз:

# Плохо: N+1 проблема
authors = Author.objects.all()
for author in authors:
    articles = author.articles.all()  # Каждый раз новый запрос

# Хорошо: 2 запроса вместо 101
authors = Author.objects.prefetch_related('articles').all()

for author in authors:
    articles = author.articles.all()  # Использует кэшированные данные

SQL запросы:

-- Запрос 1
SELECT * FROM authors;

-- Запрос 2 (одновременно для всех авторов)
SELECT * FROM articles WHERE author_id IN (1, 2, 3, ..., 100);

Для ManyToMany связей:

class Article(models.Model):
    title = models.CharField(max_length=200)
    tags = models.ManyToManyField(Tag)

# Плохо: N+1 проблема
articles = Article.objects.all()
for article in articles:
    tags = article.tags.all()  # Новый запрос для каждой статьи

# Хорошо: 2 запроса
articles = Article.objects.prefetch_related('tags').all()
for article in articles:
    tags = article.tags.all()  # Использует кэш

Решение 3: Комбинирование select_related и prefetch_related

# Загрузить автора через INNER JOIN (select_related)
# и комментарии через отдельный запрос (prefetch_related)
articles = Article.objects.select_related('author').prefetch_related('comments').all()

# 3 запроса:
# 1. SELECT articles, authors (INNER JOIN)
# 2. SELECT comments WHERE article_id IN (...)
# 3. Кэш Django объединяет результаты

Решение 4: only() и defer() для выборочных полей

Если нужны только некоторые поля, можно избежать загрузки больших полей:

# Загрузить только username и email, без bio (больших данных)
users = User.objects.only('username', 'email').all()

# Загрузить все кроме bio
users = User.objects.defer('bio').all()

# Комбинировать с select_related
articles = (
    Article.objects
    .select_related('author')
    .only('title', 'author__username')
    .all()
)

Решение 5: Raw SQL для сложных случаев

Для очень сложных запросов с множественными соединениями raw SQL может быть эффективнее:

# Одна оптимизированная SQL query вместо множества ORM запросов
from django.db import connection
from django.db.models import Prefetch

query = """
    SELECT 
        a.id, a.title, a.author_id,
        u.id, u.name,
        c.id, c.content
    FROM articles a
    JOIN users u ON a.author_id = u.id
    LEFT JOIN comments c ON a.id = c.article_id
    WHERE a.published_at IS NOT NULL
    ORDER BY a.created_at DESC
"""

with connection.cursor() as cursor:
    cursor.execute(query)
    results = cursor.fetchall()

Решение 6: Prefetch с Filter (Prefetch Object)

Для фильтрации связанных данных используй Prefetch:

from django.db.models import Prefetch

# Загрузить авторов и только их опубликованные статьи
published_articles = Article.objects.filter(status='published')
prefetch = Prefetch('articles', queryset=published_articles)

authors = Author.objects.prefetch_related(prefetch).all()

for author in authors:
    # author.articles.all() вернёт только опубликованные статьи
    print(f"{author.name}: {author.articles.all()}")

Решение 7: Annotations и Aggregations

Для подсчётов и агрегаций используй annotations:

from django.db.models import Count

# Плохо: N+1 проблема
authors = Author.objects.all()
for author in authors:
    count = author.articles.count()  # Новый запрос
    print(f"{author.name}: {count} articles")

# Хорошо: 1 запрос с COUNT
authors = Author.objects.annotate(article_count=Count('articles')).all()
for author in authors:
    print(f"{author.name}: {author.article_count} articles")

# SQL:
# SELECT authors.*, COUNT(articles.id) as article_count
# FROM authors
# LEFT JOIN articles ON authors.id = articles.author_id
# GROUP BY authors.id

Инструменты для отладки

Django Debug Toolbar:

# settings.py
INSTALLED_APPS = [
    'debug_toolbar',
    ...
]

MIDDLEWARE = [
    'debug_toolbar.middleware.DebugToolbarMiddleware',
    ...
]

Показывает все SQL запросы и время выполнения.

Явная логирование:

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

with CaptureQueriesContext(connection) as context:
    authors = Author.objects.all()
    for author in authors:
        articles = author.articles.all()

print(f"Total queries: {len(context)}")
for query in context:
    print(query['sql'])

Чеклист оптимизации

  1. Identify — используй Django Debug Toolbar для поиска N+1 проблем
  2. ForeignKey — используй select_related()
  3. ManyToMany и Reverse FK — используй prefetch_related()
  4. Условия на связи — используй Prefetch с фильтрами
  5. Агрегации — используй annotate() вместо loops
  6. Полная оптимизация — raw SQL для сложных случаев
  7. Тестирование — проверяй количество запросов в тестах

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

# ДО: 101 запрос
def get_authors_with_articles():
    authors = Author.objects.all()
    data = []
    for author in authors:
        articles = author.articles.filter(status='published')
        data.append({
            'author': author.name,
            'articles': [a.title for a in articles]
        })
    return data

# ПОСЛЕ: 2 запроса
def get_authors_with_articles():
    published_articles = Article.objects.filter(status='published')
    prefetch = Prefetch('articles', queryset=published_articles)
    
    authors = (
        Author.objects
        .prefetch_related(prefetch)
        .all()
    )
    
    data = []
    for author in authors:
        data.append({
            'author': author.name,
            'articles': [a.title for a in author.articles.all()]
        })
    return data

Проблема N+1 — одна из самых распространённых проблем производительности в Django приложениях. Её решение — ключевой навык для написания эффективного кода.

Как решается проблема N+1 запросов? | PrepBro