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

Каким образом оптимизируешь запросы к связанным моделям?

1.0 Junior🔥 111 комментариев
#Python Core

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

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

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

Оптимизация запросов к связанным моделям (N+1 Problem)

Проблема N+1 Query

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

# ❌ Плохо: N+1 запросы
from django.db import models

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

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

# В view
books = Book.objects.all()  # 1 запрос
for book in books:
    print(book.author.name)  # N запросов! (один на каждую книгу)
    # SELECT * FROM authors WHERE id = ...

Решение 1: select_related() — для ForeignKey

select_related() использует SQL JOIN для получения связанных данных в одном запросе.

# ✅ Хорошо: 1 запрос с JOIN
books = Book.objects.select_related('author')  # JOIN с authors
for book in books:
    print(book.author.name)  # Нет дополнительных запросов!

# SQL, который выполняется:
# SELECT books.*, authors.* FROM books
# JOIN authors ON books.author_id = authors.id

# Глубокая связь
comments = Comment.objects.select_related(
    'post',          # ForeignKey
    'post__author'   # ForeignKey от ForeignKey
)
for comment in comments:
    print(comment.post.author.name)  # Все в памяти

# Несколько связей
posts = Post.objects.select_related('author', 'category')

Когда использовать: ForeignKey и OneToOneField (один-к-одному).

Решение 2: prefetch_related() — для ManyToMany и Reverse FK

prefetch_related() делает отдельный запрос для каждого related set, но минимизирует их количество.

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

class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)  # Many-to-many

# ❌ Плохо: N+1
books = Book.objects.all()  # 1 запрос
for book in books:
    for author in book.authors.all():  # N запросов!
        print(author.name)

# ✅ Хорошо: 2 запроса
books = Book.objects.prefetch_related('authors')
# Query 1: SELECT * FROM books
# Query 2: SELECT * FROM authors WHERE id IN (...)
for book in books:
    for author in book.authors.all():  # Из кеша!
        print(author.name)

# Обратные связи (Reverse FK)
authors = Author.objects.prefetch_related('book_set')
for author in authors:
    for book in author.book_set.all():  # Из кеша
        print(book.title)

Когда использовать: ManyToMany и обратные ForeignKey.

Решение 3: Prefetch Objects — более сложные сценарии

from django.db.models import Prefetch

# Фильтр при prefetch
recent_comments = Comment.objects.filter(
    created_at__gte=timezone.now() - timedelta(days=7)
)

posts = Post.objects.prefetch_related(
    Prefetch('comments', queryset=recent_comments)
)

for post in posts:
    for comment in post.comments.all():  # Только последние комментарии
        print(comment.text)

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

only() — получить только нужные поля.

# ❌ Плохо: загружаем всё
books = Book.objects.all()

# ✅ Хорошо: только нужные поля
books = Book.objects.only('id', 'title')  # Исключить heavy поля

# SQL:
# SELECT id, title FROM books

# Работает с select_related
books = Book.objects.select_related('author').only(
    'id', 'title', 'author__name'  # Только эти поля из author
)

defer() — исключить тяжёлые поля.

# Исключить большие текстовые поля
posts = Post.objects.defer('content', 'html_content')
for post in posts:
    print(post.title)  # OK
    print(post.content)  # Дополнительный запрос!

Решение 5: Agregation и Annotation

from django.db.models import Count, Avg, Sum

# ❌ Плохо: N запросов для подсчёта
authors = Author.objects.all()
for author in authors:
    book_count = author.book_set.count()  # N запросов
    print(f'{author.name}: {book_count}')

# ✅ Хорошо: 1 запрос с aggregation
authors = Author.objects.annotate(
    book_count=Count('book')
)
for author in authors:
    print(f'{author.name}: {author.book_count}')  # Уже вычислено

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

# Более сложный пример
authors = Author.objects.annotate(
    book_count=Count('book'),
    avg_rating=Avg('book__rating'),
    total_sales=Sum('book__sales')
).filter(
    book_count__gte=5  # Авторы с 5+ книгами
).order_by('-total_sales')

Решение 6: Raw SQL для очень сложных запросов

# Когда ORM недостаточно
from django.db import connection

def get_author_stats():
    with connection.cursor() as cursor:
        cursor.execute("""
            SELECT 
                a.id,
                a.name,
                COUNT(b.id) as book_count,
                AVG(b.rating) as avg_rating
            FROM authors a
            LEFT JOIN books b ON a.id = b.author_id
            WHERE b.published_year >= %s
            GROUP BY a.id, a.name
            ORDER BY avg_rating DESC
            LIMIT 10
        """, [2020])
        
        columns = [col[0] for col in cursor.description]
        return [
            dict(zip(columns, row))
            for row in cursor.fetchall()
        ]

Решение 7: Кеширование результатов

from django.core.cache import cache
from django.views.decorators.cache import cache_page

# На уровне вью
@cache_page(60)  # Кешировать на 60 сек
def book_list(request):
    books = Book.objects.select_related('author')
    return render(request, 'books.html', {'books': books})

# Ручное кеширование
def get_author_with_books(author_id):
    cache_key = f'author:{author_id}:with_books'
    data = cache.get(cache_key)
    
    if data is None:
        author = Author.objects.get(id=author_id)
        author.books = author.book_set.all()
        data = {'author': author, 'books': author.books}
        cache.set(cache_key, data, 3600)  # Кешировать на 1 час
    
    return data

Решение 8: Database Query Logs и Profiling

# Видеть все запросы (DEBUG mode)
from django.conf import settings
from django.test.utils import override_settings

if settings.DEBUG:
    from django.db import connection
    from django.test import TestCase
    
    class OptimizationTest(TestCase):
        def test_book_list_queries(self):
            from django.test.utils import CaptureQueriesContext
            
            with CaptureQueriesContext(connection) as context:
                books = Book.objects.select_related('author')
                list(books)  # Выполнить запрос
            
            print(f'Количество запросов: {len(context)}')
            assert len(context) == 1, f'Expected 1 query, got {len(context)}'

# Инструмент django-debug-toolbar
# pip install django-debug-toolbar
# Добавить в INSTALLED_APPS и MIDDLEWARE
# Показывает все запросы, время выполнения, profile

Решение 9: SQLAlchemy (для non-Django проектов)

from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, joinedload, selectinload
from sqlalchemy.orm import Session

Base = declarative_base()

class Author(Base):
    __tablename__ = 'authors'
    id = Column(Integer, primary_key=True)
    name = Column(String)
    books = relationship('Book', back_populates='author')

class Book(Base):
    __tablename__ = 'books'
    id = Column(Integer, primary_key=True)
    title = Column(String)
    author_id = Column(Integer, ForeignKey('authors.id'))
    author = relationship('Author', back_populates='books')

engine = create_engine('sqlite:///library.db')
session = Session(engine)

# ❌ Плохо: N+1
books = session.query(Book).all()
for book in books:
    print(book.author.name)  # N запросов

# ✅ Хорошо: joinedload (JOIN)
books = session.query(Book).options(joinedload(Book.author)).all()
for book in books:
    print(book.author.name)  # 1 запрос

# selectinload для ManyToMany
authors = session.query(Author).options(
    selectinload(Author.books)
).all()
for author in authors:
    for book in author.books:
        print(book.title)  # 2 запроса: 1 для авторов, 1 для книг

Практический чеклист оптимизации

  1. Выявить N+1:

    • Запустить с django-debug-toolbar или логами
    • Посчитать количество запросов
  2. Применить select_related:

    • Для ForeignKey и OneToOneField
    • Когда всегда нужны связанные данные
  3. Применить prefetch_related:

    • Для ManyToMany
    • Для обратных ForeignKey
  4. Использовать only/defer:

    • Исключить тяжёлые поля (text, JSON)
    • Когда нужны только определённые колонки
  5. Agregation вместо Count:

    • Использовать Count(), Sum(), Avg() в базе
    • Не загружать в Python для подсчёта
  6. Кеширование:

    • Часто читаемые данные
    • Дорогие вычисления
  7. Индексы:

    • Индексировать внешние ключи
    • Индексировать часто фильтруемые поля
  8. Мониторинг:

    • Постоянно профилировать
    • Настраивать по результатам

Реальный пример оптимизации

# ❌ До оптимизации: 100+ запросов
def get_posts_feed():
    posts = Post.objects.all()[:20]
    return posts

# ✅ После оптимизации: 4 запроса
def get_posts_feed():
    posts = Post.objects.select_related(
        'author',
        'category'
    ).prefetch_related(
        'comments__author',
        'tags'
    ).annotate(
        comment_count=Count('comments'),
        like_count=Count('likes')
    ).only(
        'id', 'title', 'author_id', 'category_id',
        'author__name', 'author__avatar_url',
        'category__name'
    )[:20]
    return posts

Вывод: Правильная оптимизация запросов может снизить их количество с сотен до единиц, что критично для performance.

Каким образом оптимизируешь запросы к связанным моделям? | PrepBro