Перечислите основные методы оптимизации запросов в Django ORM

Ответ

Основная цель оптимизации — сокращение количества запросов к базе данных (решение проблемы "N+1") и уменьшение объема передаваемых данных.

  1. *`select_related(fields)** Оптимизирует запросы к связанным объектам черезForeignKeyилиOneToOne. Он "подтягивает" связанные данные однимSQL JOIN` запросом, предотвращая дополнительные запросы в цикле.

    # Плохо: N+1 запросов (1 для постов + N для авторов)
    for post in Post.objects.all():
        print(post.author.name)
    
    # Хорошо: 1 запрос с JOIN
    for post in Post.objects.select_related('author').all():
        print(post.author.name)
  2. *`prefetch_related(lookups)** Используется дляManyToManyFieldи обратныхForeignKey. В отличие отselect_related, он делает отдельный запрос для связанных объектов и "склеивает" данные в Python. Это эффективнее, чемJOIN` для связей "многие-ко-многим".

    # 1 запрос для постов + 1 запрос для всех тегов
    posts = Post.objects.prefetch_related('tags').all()
  3. only(*fields) и defer(*fields) Позволяют загружать только указанные поля модели (only) или все, кроме указанных (defer). Это сокращает объем передаваемых данных и потребление памяти.

    # Загрузить только заголовки
    titles = Post.objects.only('title')
  4. values() и values_list() Возвращают словари или кортежи вместо полноценных объектов моделей. Это значительно быстрее, если вам нужны только данные и не требуются методы модели.

  5. annotate() и aggregate() Переносят вычисления (например, COUNT, SUM, AVG) на сторону базы данных, что намного эффективнее, чем делать это в Python.

    from django.db.models import Count
    # Получить авторов с количеством их постов
    authors = Author.objects.annotate(post_count=Count('posts'))
  6. exists() и count() Для проверки наличия записи используйте exists(), а для подсчета — count(). Они выполняют более легковесные SQL-запросы, чем загрузка всего QuerySet.

  7. bulk_create() и bulk_update() Для массового создания или обновления объектов используйте эти методы, чтобы выполнить операцию одним запросом вместо множества.

Рекомендация: Всегда анализируйте запросы с помощью django-debug-toolbar или connection.queries для выявления узких мест.

Ответ 18+ 🔞

А, слушай, вот это тема, про которую я могу говорить часами, потому что каждый второй проект на Django спотыкается об одно и то же — про ебаную оптимизацию запросов! Ну, точнее, про её полное отсутствие. Люди пишут код, а потом удивляются, почему страница грузится как черепаха в сиропе. А всё из-за классической хуйни под названием "Проблема N+1". Представь: ты получаешь список постов, а потом в цикле для каждого поста лезешь в базу за автором. Один запрос за постами, и ещё N запросов за авторами. Пиздец, а не архитектура.

Ну ладно, хватит ныть, давай по делу. Django, он хоть и медленный на вид, но даёт нам кучу инструментов, чтобы не выглядеть полными даунами.

1. select_related(*fields)

Это твой лучший друг для связей ForeignKey или OneToOne. Он делает один здоровенный JOIN в SQL и сразу притаскивает все связанные данные. Вместо того чтобы дёргать базу на каждом чихе.

# Пиздец как плохо: запрос за постами, а потом для каждого поста — отдельный запрос за автором. N+1 в чистом виде.
for post in Post.objects.all():
    print(post.author.name)  # ОПА! Новый запрос в БД на каждой итерации!

# А вот так — красиво и по-взрослому. Один запрос, и все авторы уже прицеплены.
for post in Post.objects.select_related('author').all():
    print(post.author.name)  # Данные уже в памяти, нихуя не грузим!

2. prefetch_related(*lookups)

А это уже для более сложных связей — ManyToManyField или обратных ForeignKey. JOIN тут может превратиться в ад, поэтому prefetch_related действует хитрее: делает два запроса (один за основной моделью, второй за связанной), а потом в Питоне всё аккуратно склеивает. Умно, блядь.

# Всего 2 запроса: один для всех постов, второй — для ВСЕХ их тегов. Никакого N+1.
posts = Post.objects.prefetch_related('tags').all()
for post in posts:
    for tag in post.tags.all():  # Теги уже здесь, в памяти!
        print(tag.name)

3. only(*fields) и defer(*fields)

Зачем тащить из базы всю хуйню, если нужна только пара полей? Особенно если в модели есть здоровенные TextField. only загружает только указанные поля, defer — наоборот, всё, кроме указанных. Экономия трафика и памяти — овердохуищная.

# Загрузим только заголовки, а всё остальное — нахуй не надо.
titles = Post.objects.only('title')

4. values() и values_list()

А если тебе вообще не нужен объект модели со всеми его методами и прибамбасами? Если нужны просто голые данные? Вот тут эти методы спасают — возвращают словарики или кортежи. Скорость вырастает в разы, потому что ORM не тратит время на создание объектов.

# Просто список словарей с id и title. Быстро и сердито.
post_data = Post.objects.values('id', 'title')

5. annotate() и aggregate()

Это чтобы не быть идиотом и не считать что-то в цикле на Питоне. База данных считает в сотни раз быстрее. annotate — добавляет вычисляемое поле к каждому объекту (например, количество комментов). aggregate — считает общую статистику по всему QuerySet (сумму, среднее и т.д.).

from django.db.models import Count
# Каждому автору добавим поле post_count. Всё считается одним запросом на стороне БД.
authors = Author.objects.annotate(post_count=Count('posts'))

6. exists() и count()

Классика: нужно проверить, есть ли хоть одна запись? Не делай if queryset:, блядь! Это выполнит запрос и загрузит ВСЕ данные. Используй exists() — он сделает легковесный SELECT 1 ... LIMIT 1. Аналогично с подсчётом — count() сделает SELECT COUNT(*), а не SELECT *.

# Пиздец как эффективно
if SomeModel.objects.filter(condition).exists():
    print("Что-то есть, ёпта!")

total = SomeModel.objects.count()  # Быстро и без лишней нагрузки.

7. bulk_create() и bulk_update()

Ну это вообще must-have, когда нужно впихнуть или обновить кучу записей. Вместо тысячи отдельных INSERT или UPDATE — один здоровенный запрос. Разница в скорости — небо и земля, ядрёна вошь.

# Создаём 1000 объектов одним махом
objects_to_create = [MyModel(name=f'Object {i}') for i in range(1000)]
MyModel.objects.bulk_create(objects_to_create)

И главная рекомендация, блядь: Не гадай на кофейной гуще! Поставь django-debug-toolbar или смотри в connection.queries. Увидишь, какие запросы летят, сколько их и как долго выполняются. А то будешь как тот Герасим — молча страдать, а проблема-то в ебучем N+1, который на виду. Не будь немым, оптимизируй!