Python: Django ORM
Теория: Эффективное использование ORM
Как и любое средство повышения уровня абстракции, ORM может выдавать не самые эффективные запросы в некоторых случаях. К сожалению, по-другому быть не может: тонко настроенные SQL-запросы находятся на существенно более низком уровне. В Django ORM можно переписывать код запроса руками, но есть ли в этом необходимость? Вовсе нет.
Для начала прочтем отрывок из документации к фреймворку:
- Сначала измерьте. У любого QuerySet можно запросить описание предполагаемых запросов с помощью вызова метода
.explain(). Тут пригодится умение читать SQL и понимать то, о чем рассказывает черезexplainваша СУБД - Добавьте индексы. SQL explain часто может сказать, что в каком-то месте делается "full scan" то есть полный перебор, а это означает, что где-то не хватает индексов. Стоит подумать о том, чтобы оные добавить. Подробности можно почитать в документации
- Делайте как можно больше работы силами СУБД. Аннотируйте, агрегируйте. СУБД знает, как это оптимизировать и как кэшировать результаты.
- В крайнем случае пишите необходимый минимум SQL.
- Знайте, как работают QuerySets и помните:
- Насколько QuerySets ленив — он не делает лишних запросов, пока вы не попросите
- В какой момент QuerySet вычисляется (финализируется)
- Каким образом данные представлены в памяти
- Как QuerySet кеширует результаты запросов и подзапросов
Следует помнить, что многие ситуации разрешаются задолго до того, как вы дойдете до написания SQL.
Explain
Один из ключевых приемов для анализа запросов это функция EXPLAIN. Он показывает, как база данных планирует выполнить ваш запрос. Он раскрывает такие важные детали, как использование индексов, порядок соединения таблиц и предполагаемую стоимость операций. В Django метод .explain() предоставляет удобный доступ к этой функциональности, помогая понять, почему некоторые запросы могут работать медленнее других и как их можно оптимизировать.
EXPLAIN выведет структуру запроса конкретной базы данных. Выше пример для Postgres, в другой базе вывод будет своим.
Ограничение состава загружаемых данных
Довольно часто нам не требуются все поля модели — нужна всего пара полей из нескольких десятков. Тут пригодится метод .values_list(имена, полей), который возвращает новый QuerySet. Его элементами будут кортежи со значениями указанных полей в указанном же порядке. Такой QuerySet удобно обходить в цикле с одновременной распаковкой:
Если значений нужно не два и не три, то есть смысл использовать метод .values(имена, полей). При обходе итогового QuerySet он даст уже не кортежи, а словари с ключами, совпадающими с именами указанных полей.
Однако ни кортежи, ни словари не дают воспользоваться методами модели, а в некоторых ситуациях это все-таки требуется. Если точно известно, какие поля понадобятся при работе с моделью и при вызове ее методов, подойдет метод .only(имена, полей): возращаемый им QuerySet выдает объекты модели, но в каждом объекте заполнены только указанные поля. Так мы можем использовать все возможности модели. Но следует помнить, что первое же обращение к полю, не указанному в вызове .only(), породит запрос. Он сработает для текущего объекта — запросит данные для этого поля и запомнит их на будущее:
Использование .only() отлично показывает всю высокоуровневость ORM: можно просто обращаться к полям объекта и не думать, что и когда подгружается. Еще view выводит посты блога в виде списка диалогов, причем выводит тело только для пары первых пунктов. Это очень лаконичное решение с точки зрения кода, которое еще и не слишком нагружает базу.
Ранняя загрузка связанных данных
Проблема N+1 запросов - это классическая ловушка производительности при работе с ORM. Она возникает, когда код сначала делает один запрос для получения списка объектов, а затем для каждого объекта выполняет дополнительный запрос для получения связанных данных.
Например, мы открываем список из 100 постов блога, и для каждого поста нужно показать имя автора. Наивный подход приведет к тому, что сначала будет выполнен один запрос для получения постов, а затем для каждого поста будет выполнен отдельный запрос для получения информации об авторе. В итоге получится 101 запрос к базе данных (1 + 100), отсюда и название "N+1".
Вот как это выглядит в коде:
Здесь один запрос получает все посты, а затем для каждого поста запрашивается автор.
Нельзя сказать, что тут все плохо. Авторы подгружаются по мере необходимости, если мы хотим вывести список заголовков постов и показать автора первых двух постов, то это приемлемый код.
Если же все посты нужны вместе с авторами, нужно использовать метод .select_related(), который эквивалентен JOIN в SQL. Если при вызове метода не указывать аргументы, то будут присоединены все связанные таблицы. Если же указать имена связей, то указанные таблицы подгрузятся сразу, остальные — как обычно, по требованию. Это достаточно гибкое средство, особенно в сочетании с другими способами оптимизации:
Обратите внимание, что первый запрос содержал только указанные поля — пусть даже и в двух таблицах — а также служебные поля вроде id и поля для связи таблиц. При этом с точки зрения наблюдателя post выглядит как цельный объект модели Post, а его атрибут .author — как цельный объект модели User. Все дополнительные данные прозрачно подгружаются по мере необходимости.
Кроме select_related(), в Django есть еще один похожий метод — prefetch_related(). С помощью обоих методов можно работать со связанными объектами, но немного в разных сценариях:
-
Метод
select_related()используется для отношений типа "один к одному" (OneToOne) и "многие к одному" (ForeignKey). Например, такое отношение есть между постом и автором в блоге, потому что каждый пост написан одним автором. Чтобы получить информацию об авторе каждого поста, можно добавитьselect_related()в тот же запрос к базе данных. Методselect_related()работает через выполнениеSQL JOINи включение полей связанного объекта в операторSELECT. -
Метод
prefetch_related()используется для отношений типа "один ко многим" и "многие ко многим" (ManyToManyField). Наглядный пример — пост в блоге, у которого может быть много комментариев (один ко многим), или теги постов (многие ко многим). Методprefetch_related()выполняет отдельные запросы для каждого отношения и выполняет объединение уже в Python. Так мы заранее извлекаем связанные объекты, что помогает избежать проблемы N+1 запросов
Оба метода помогают избежать проблемы N+1, когда итерация по связанным объектам вызывает дополнительный запрос для каждого объекта. Но все таки один метод может быть более подходящим, чем другой — все зависит от типа отношения и конкретного случая.
Проще говоря, оба метода позволяют сказать: «Я получаю данные и знаю, что еще мне понадобятся эти другие связанные данные, поэтому получи все сразу, а не ходи туда-сюда в базу данных».
Самостоятельная работа
- На примере моделей из учебного проекта постройте несколько сложных запросов и понаблюдайте, какие запросы и в какой момент делает ORM.
- Попробуйте пооптимизировать запросы с помощью описанных в уроке средств.


