Оптимизация запросов Django ORM

Неэффективные запросы к базе данных - главная причина медленной работы Django приложений. Правильная оптимизация может ускорить работу в 10-100 раз.

Проблема N+1 запросов

N+1 проблема возникает когда для каждого объекта в QuerySet выполняется дополнительный запрос для получения связанных данных.

1# Неправильно - N+1 запросов
2books = Book.objects.all()
3for book in books:
4    print(f"{book.title} - {book.author.name}")  # Каждый раз новый запрос!
5
6# Правильно - используем select_related
7books = Book.objects.select_related('author').all()
8for book in books:
9    print(f"{book.title} - {book.author.name}")  # Автор уже загружен

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

1. Оптимизация ForeignKey связей (select_related)

 1# Модели для примера
 2class Author(models.Model):
 3    name = models.CharField(max_length=100)
 4    email = models.EmailField()
 5    bio = models.TextField()
 6
 7class Book(models.Model):
 8    title = models.CharField(max_length=200)
 9    author = models.ForeignKey(Author, on_delete=models.CASCADE)
10    published_date = models.DateField()
11    price = models.DecimalField(max_digits=10, decimal_places=2)
12
13class Review(models.Model):
14    book = models.ForeignKey(Book, on_delete=models.CASCADE)
15    reviewer_name = models.CharField(max_length=100)
16    rating = models.IntegerField()
17    comment = models.TextField()
18
19# Неоптимизированный код
20books = Book.objects.all()
21for book in books:
22    print(f"Книга: {book.title}")
23    print(f"Автор: {book.author.name}")  # N+1 запрос!
24    print(f"Email автора: {book.author.email}")  # Еще один запрос!
25    print("---")
26
27# Оптимизированный код с select_related
28books = Book.objects.select_related('author').all()
29for book in books:
30    print(f"Книга: {book.title}")
31    print(f"Автор: {book.author.name}")  # Данные уже загружены
32    print(f"Email автора: {book.author.email}")  # Данные уже загружены
33    print("---")
34
35# Глубокие связи (несколько уровней)
36reviews = Review.objects.select_related('book__author').all()
37for review in reviews:
38    print(f"Отзыв на книгу: {review.book.title}")
39    print(f"Автор: {review.book.author.name}")  # Все данные загружены одним запросом
40    print(f"Рейтинг: {review.rating}")
41    print("---")

2. Оптимизация ManyToMany связей (prefetch_related)

 1# Модели с ManyToMany
 2class Category(models.Model):
 3    name = models.CharField(max_length=100)
 4    description = models.TextField()
 5
 6class Book(models.Model):
 7    title = models.CharField(max_length=200)
 8    author = models.ForeignKey(Author, on_delete=models.CASCADE)
 9    categories = models.ManyToManyField(Category)
10    published_date = models.DateField()
11
12# Неоптимизированный код
13books = Book.objects.all()
14for book in books:
15    print(f"Книга: {book.title}")
16    for category in book.categories.all():  # N+1 запрос!
17        print(f"  Категория: {category.name}")
18    print("---")
19
20# Оптимизированный код с prefetch_related
21books = Book.objects.prefetch_related('categories').all()
22for book in books:
23    print(f"Книга: {book.title}")
24    for category in book.categories.all():  # Данные уже загружены
25        print(f"  Категория: {category.name}")
26    print("---")
27
28# Комбинирование select_related и prefetch_related
29books = Book.objects.select_related('author').prefetch_related('categories').all()
30for book in books:
31    print(f"Книга: {book.title}")
32    print(f"Автор: {book.author.name}")  # ForeignKey - select_related
33    for category in book.categories.all():  # ManyToMany - prefetch_related
34        print(f"  Категория: {category.name}")
35    print("---")

3. Оптимизация с помощью only() и defer()

 1# Загружаем только нужные поля
 2# Полезно для больших текстовых полей или когда нужны только определенные данные
 3
 4# Загружаем только title и author__name
 5books = Book.objects.select_related('author').only('title', 'author__name').all()
 6for book in books:
 7    print(f"{book.title} - {book.author.name}")
 8    # book.description будет недоступно (вызовет ошибку)
 9    # book.author.email будет недоступно
10
11# Откладываем загрузку больших полей
12books = Book.objects.defer('description', 'content').all()
13for book in books:
14    print(f"Название: {book.title}")
15    # book.description загрузится только при обращении к нему
16
17# Комбинирование only и defer
18books = Book.objects.only('title', 'author__name').defer('author__bio').all()
19for book in books:
20    print(f"{book.title} - {book.author.name}")
21    # author.bio будет загружено только при обращении

4. Оптимизация агрегации и аннотаций

 1from django.db.models import Count, Sum, Avg, Max, Min
 2
 3# Неоптимизированный код
 4authors = Author.objects.all()
 5for author in authors:
 6    book_count = author.book_set.count()  # N+1 запрос!
 7    total_price = sum(book.price for book in author.book_set.all())  # Еще N+1 запрос!
 8    print(f"{author.name}: {book_count} книг, общая стоимость: {total_price}")
 9
10# Оптимизированный код с annotate
11authors = Author.objects.annotate(
12    book_count=Count('book'),
13    total_price=Sum('book__price'),
14    avg_price=Avg('book__price'),
15    max_price=Max('book__price'),
16    min_price=Min('book__price')
17).all()
18
19for author in authors:
20    print(f"{author.name}: {author.book_count} книг")
21    print(f"  Общая стоимость: {author.total_price or 0}")
22    print(f"  Средняя цена: {author.avg_price or 0}")
23    print(f"  Максимальная цена: {author.max_price or 0}")
24    print(f"  Минимальная цена: {author.min_price or 0}")
25    print("---")
26
27# Группировка по категориям
28category_stats = Category.objects.annotate(
29    book_count=Count('book'),
30    avg_price=Avg('book__price')
31).filter(book_count__gt=0).order_by('-book_count')
32
33for category in category_stats:
34    print(f"Категория: {category.name}")
35    print(f"  Книг: {category.book_count}")
36    print(f"  Средняя цена: {category.avg_price or 0}")
37    print("---")

5. Оптимизация с помощью bulk операций

 1# Неоптимизированное создание объектов
 2for i in range(1000):
 3    Book.objects.create(
 4        title=f'Книга {i}',
 5        author=author,
 6        published_date='2024-01-01',
 7        price=10.00
 8    )  # 1000 отдельных INSERT запросов!
 9
10# Оптимизированное создание с bulk_create
11books_to_create = []
12for i in range(1000):
13    books_to_create.append(Book(
14        title=f'Книга {i}',
15        author=author,
16        published_date='2024-01-01',
17        price=10.00
18    ))
19
20Book.objects.bulk_create(books_to_create)  # Один запрос для всех книг!
21
22# Bulk обновление
23books = Book.objects.filter(price__lt=20.00)
24for book in books:
25    book.price = book.price * 1.1  # Увеличиваем цену на 10%
26
27Book.objects.bulk_update(books, ['price'])  # Один UPDATE запрос!
28
29# Bulk удаление
30old_books = Book.objects.filter(published_date__year__lt=2020)
31old_books.delete()  # Один DELETE запрос!

6. Оптимизация с помощью raw SQL

 1# Когда ORM не справляется со сложными запросами
 2from django.db import connection
 3
 4# Сложный запрос с JOIN и подзапросами
 5with connection.cursor() as cursor:
 6    cursor.execute("""
 7        SELECT
 8            a.name as author_name,
 9            COUNT(b.id) as book_count,
10            AVG(b.price) as avg_price,
11            SUM(b.price) as total_revenue
12        FROM myapp_author a
13        LEFT JOIN myapp_book b ON a.id = b.author_id
14        WHERE b.published_date >= '2020-01-01'
15        GROUP BY a.id, a.name
16        HAVING COUNT(b.id) > 5
17        ORDER BY total_revenue DESC
18    """)
19
20    results = cursor.fetchall()
21    for row in results:
22        print(f"Автор: {row[0]}")
23        print(f"  Книг: {row[1]}")
24        print(f"  Средняя цена: {row[2]}")
25        print(f"  Общий доход: {row[3]}")
26        print("---")
27
28# Или используй raw() для моделей
29books = Book.objects.raw("""
30    SELECT b.*, a.name as author_name
31    FROM myapp_book b
32    JOIN myapp_author a ON b.author_id = a.id
33    WHERE b.price > %s
34    ORDER BY b.published_date DESC
35""", [20.00])
36
37for book in books:
38    print(f"{book.title} - {book.author_name}")

Профилирование и мониторинг запросов

Настройка логирования SQL запросов

 1# settings.py
 2LOGGING = {
 3    'version': 1,
 4    'disable_existing_loggers': False,
 5    'handlers': {
 6        'console': {
 7            'class': 'logging.StreamHandler',
 8        },
 9    },
10    'loggers': {
11        'django.db.backends': {
12            'handlers': ['console'],
13            'level': 'DEBUG',
14        },
15    },
16}
17
18# Или используй django-debug-toolbar для разработки
19INSTALLED_APPS = [
20    'debug_toolbar',
21]
22
23MIDDLEWARE = [
24    'debug_toolbar.middleware.DebugToolbarMiddleware',
25]
26
27INTERNAL_IPS = [
28    '127.0.0.1',
29]

Мониторинг производительности в коде

 1import time
 2from django.db import connection
 3
 4# Функция для измерения времени выполнения запросов
 5def measure_queries(func):
 6    def wrapper(*args, **kwargs):
 7        initial_queries = len(connection.queries)
 8        start_time = time.time()
 9
10        result = func(*args, **kwargs)
11
12        end_time = time.time()
13        final_queries = len(connection.queries)
14
15        print(f"Функция {func.__name__}:")
16        print(f"  Время выполнения: {end_time - start_time:.4f} секунд")
17        print(f"  Количество запросов: {final_queries - initial_queries}")
18        print(f"  Общее время SQL: {sum(float(q['time']) for q in connection.queries[initial_queries:final_queries]):.4f} секунд")
19
20        return result
21    return wrapper
22
23# Использование декоратора
24@measure_queries
25def get_books_with_authors():
26    return Book.objects.select_related('author').all()
27
28@measure_queries
29def get_books_without_optimization():
30    return Book.objects.all()
31
32# Сравниваем производительность
33print("С оптимизацией:")
34books1 = get_books_with_authors()
35
36print("\nБез оптимизации:")
37books2 = get_books_without_optimization()

Создание и использование индексов

Добавление индексов в модели

 1class Book(models.Model):
 2    title = models.CharField(max_length=200, db_index=True)  # Индекс для поиска
 3    author = models.ForeignKey(Author, on_delete=models.CASCADE, db_index=True)
 4    published_date = models.DateField(db_index=True)  # Индекс для фильтрации по дате
 5    price = models.DecimalField(max_length=10, decimal_places=2, db_index=True)
 6    isbn = models.CharField(max_length=13, unique=True)  # Уникальный индекс
 7
 8    class Meta:
 9        # Составной индекс для часто используемых комбинаций
10        indexes = [
11            models.Index(fields=['author', 'published_date']),
12            models.Index(fields=['price', 'published_date']),
13            # Индекс для полнотекстового поиска (PostgreSQL)
14            models.Index(fields=['title'], name='book_title_gin_idx'),
15        ]
16
17# Создание индекса через миграцию
18class Migration(migrations.Migration):
19    dependencies = [
20        ('myapp', '0001_initial'),
21    ]
22
23    operations = [
24        migrations.AddIndex(
25            model_name='book',
26            index=models.Index(
27                fields=['author', 'published_date'],
28                name='book_author_date_idx'
29            ),
30        ),
31    ]

Анализ использования индексов

 1# PostgreSQL - анализ запроса
 2from django.db import connection
 3
 4with connection.cursor() as cursor:
 5    cursor.execute("""
 6        EXPLAIN (ANALYZE, BUFFERS)
 7        SELECT * FROM myapp_book
 8        WHERE author_id = 1 AND published_date > '2020-01-01'
 9    """)
10
11    plan = cursor.fetchall()
12    for row in plan:
13        print(row[0])
14
15# MySQL - анализ запроса
16with connection.cursor() as cursor:
17    cursor.execute("""
18        EXPLAIN FORMAT=JSON
19        SELECT * FROM myapp_book
20        WHERE author_id = 1 AND published_date > '2020-01-01'
21    """)
22
23    plan = cursor.fetchall()
24    print(plan)

Кэширование запросов

Настройка кэширования

 1# settings.py
 2CACHES = {
 3    'default': {
 4        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
 5        'LOCATION': 'redis://127.0.0.1:6379/1',
 6    }
 7}
 8
 9# Кэширование в коде
10from django.core.cache import cache
11from django.views.decorators.cache import cache_page
12
13# Кэширование view
14@cache_page(60 * 15)  # Кэш на 15 минут
15def book_list(request):
16    books = Book.objects.select_related('author').all()
17    return render(request, 'books/list.html', {'books': books})
18
19# Кэширование QuerySet
20def get_popular_books():
21    cache_key = 'popular_books'
22    books = cache.get(cache_key)
23
24    if books is None:
25        books = Book.objects.select_related('author').annotate(
26            review_count=Count('review')
27        ).order_by('-review_count')[:10]
28
29        # Кэшируем на 1 час
30        cache.set(cache_key, books, 60 * 60)
31
32    return books
33
34# Кэширование с версионированием
35def get_author_books(author_id):
36    cache_key = f'author_books_{author_id}'
37    books = cache.get(cache_key)
38
39    if books is None:
40        books = Book.objects.filter(author_id=author_id).select_related('author')
41        cache.set(cache_key, books, 60 * 30)  # 30 минут
42
43    return books
44
45# Инвалидация кэша при изменении данных
46def invalidate_author_cache(author_id):
47    cache_key = f'author_books_{author_id}'
48    cache.delete(cache_key)

Лучшие практики оптимизации

  • Всегда используй select_related для ForeignKey - это самый простой способ избежать N+1
  • Используй prefetch_related для ManyToMany - загружай связанные объекты одним запросом
  • Применяй only() и defer() для больших полей - загружай только нужные данные
  • Используй bulk операции для массовых изменений - это в разы быстрее
  • Создавай индексы для часто фильтруемых полей - особенно для дат и внешних ключей
  • Кэшируй часто запрашиваемые данные - используй Redis или Memcached
  • Профилируй запросы в development - используй django-debug-toolbar
  • Мониторь производительность в production - следи за временем выполнения запросов

Частые ошибки и их решения

Ошибка: "FieldError: Cannot resolve keyword"

1# Проблема: неправильное использование select_related
2# Неправильно
3books = Book.objects.select_related('categories').all()  # Ошибка!
4
5# Правильно
6books = Book.objects.prefetch_related('categories').all()  # ManyToMany требует prefetch_related

Ошибка: "QuerySet is not iterable"

 1# Проблема: использование only() с полями, которые не загружены
 2books = Book.objects.only('title').all()
 3
 4# Неправильно
 5for book in books:
 6    print(book.author.name)  # Ошибка! author не загружен
 7
 8# Правильно
 9books = Book.objects.select_related('author').only('title', 'author__name').all()
10for book in books:
11    print(book.author.name)  # Работает

FAQ

Q: Когда использовать select_related?
A: Для ForeignKey и OneToOneField когда нужны связанные объекты. Это загружает данные одним JOIN запросом.

Q: Когда использовать prefetch_related?
A: Для ManyToManyField и reverse ForeignKey. Это загружает связанные объекты отдельным запросом.

Q: Как определить N+1 проблему?
A: Включи логирование SQL запросов и посмотри на количество запросов при итерации по QuerySet.

Q: Когда использовать raw SQL?
A: Для сложных запросов, которые сложно выразить через ORM, или когда нужна максимальная производительность.

Q: Как оптимизировать поиск по тексту?
A: Используй полнотекстовый поиск PostgreSQL, создавай GIN индексы для текстовых полей.

Q: Можно ли кэшировать QuerySet?
A: Да, но лучше кэшировать результаты, а не сам QuerySet. Используй cache.get() и cache.set().