Как и любое средство повышения уровня абстракции, ORM может выдавать не самые эффективные запросы в некоторых случаях. К сожалению, по-другому быть не может: тонко настроенные SQL-запросы находятся на существенно более низком уровне. В Django ORM можно переписывать код запроса руками, но есть ли в этом необходимость? Вовсе нет.
Для начала прочтем отрывок из документации к фреймворку:
- Сначала измерьте. У любого QuerySet можно запросить описание предполагаемых запросов с помощью вызова метода
.explain()
. Тут пригодится умение читать SQL и понимать то, о чем рассказывает черезexplain
ваша СУБД - Добавьте индексы. SQL explain часто может сказать, что в каком-то месте делается "full scan" то есть полный перебор, а это означает, что где-то не хватает индексов. Стоит подумать о том, чтобы оные добавить. Подробности можно почитать в документации
- Делайте как можно больше работы силами СУБД. Аннотируйте, агрегируйте. СУБД знает, как это оптимизировать и как кэшировать результаты.
- В крайнем случае пишите необходимый минимум SQL.
- Знайте, как работают QuerySets и помните:
- Насколько QuerySets ленив — он не делает лишних запросов, пока вы не попросите
- В какой момент QuerySet вычисляется (финализируется)
- Каким образом данные представлены в памяти
- Как QuerySet кеширует результаты запросов и подзапросов
Следует помнить, что многие ситуации разрешаются задолго до того, как вы дойдете до написания SQL.
Построитель запросов Django ORM позволяет решать многие задачи, не сильно усложняя код запросов. Разберем два класса улучшений: ограничение состава запрашиваемых данных и раннюю загрузку связанных данных.
Ограничение состава загружаемых данных
Довольно часто нам не требуются все поля модели — нужна всего пара полей из нескольких десятков. Тут пригодится метод .values_list(имена, полей)
, который возвращает новый QuerySet. Его элементами будут кортежи со значениями указанных полей в указанном же порядке. Такой QuerySet удобно обходить в цикле с одновременной распаковкой:
for pid, title in Post.objects.values_list('id', 'title'):
print(pid, '|', title)
# SELECT "blog_post"."id",
# "blog_post"."title"
# FROM "blog_post"
# Execution time: 0.000571s [Database: default]
# => 1 | Intro
# => 2 | Update
Если значений нужно не два и не три, то есть смысл использовать метод .values(имена, полей)
. При обходе итогового QuerySet он даст уже не кортежи, а словари с ключами, совпадающими с именами указанных полей.
Однако ни кортежи, ни словари не дают воспользоваться методами модели, а в некоторых ситуациях это все-таки требуется. Если точно известно, какие поля понадобятся при работе с моделью и при вызове ее методов, подойдет метод .only(имена, полей)
: возращаемый им QuerySet выдает объекты модели, но в каждом объекте заполнены только указанные поля. Так мы можем использовать все возможности модели. Но следует помнить, что первое же обращение к полю, не указанному в вызове .only()
, породит запрос. Он сработает для текущего объекта — запросит данные для этого поля и запомнит их на будущее:
ps = Post.objects.only('title')
post = ps[0] # Первый пост, загружаются только заголовки (и id)
# SELECT "blog_post"."id",
# "blog_post"."title"
# FROM "blog_post"
# LIMIT 1
# Execution time: 0.000327s [Database: default]
post.title
# => 'Intro'
post.body # Поле потребует загрузки