ORM, как и любое средство повышения уровня абстракции, имеет свойство выдавать не самые эффективные запросы в некоторых случаях. По-другому, увы, и не может быть: тонко настроенные SQL-запросы находятся на существенно более низком уровне. Но так ли необходимо переписывать код запроса руками — а Django ORM позволяет такое делать — сразу же как проявится "узкое место"? Вовсе нет!
Вот что говорит документация к фреймворку (дана выдержка):
.explain()
. Тут пригодится умение читать SQL и понимать то, о чём рассказывает через explain
ваша СУБД.Следует помнить, что многие ситуации разрешаются задолго до того, как вы дойдёте до написания 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 # поле потребует загрузки
# SELECT "blog_post"."id",
# "blog_post"."body"
# FROM "blog_post"
# WHERE "blog_post"."id" = 1
# LIMIT 21
# Execution time: 0.000262s [Database: default]
# => 'Hi, my name is Bob!'
Использование .only()
отлично показывает всю высокоуровневость ORM: вы не думаете о том, что и когда подгружается, а просто обращаетесь к полям объекта. И view, которая выводит посты блога в виде списка диалогов и только для пары первых выводит тело, будет и с точки зрения кода лаконичной и не слишком нагрузит базу!
Если написать такой цикл:
for p in Post.objects.all():
print(p.title, p.creator.email)
# SELECT ...
# FROM "blog_post"
# Execution time: 0.002823s [Database: default]
# SELECT ...
# FROM "blog_user"
# WHERE "blog_user"."id" = 1
# Execution time: 0.000459s [Database: default]
# Intro => bob@blogs.org
# SELECT ...
# FROM "blog_user"
# WHERE "blog_user"."id" = 1
# LIMIT 21
# Execution time: 0.000123s [Database: default]
# Update => bob@blogs.org
Вы увидите тот самый случай "N+1", который был упомянут в уроке про аннотирование. Здесь один запрос получает все посты, а затем для каждого поста запрашивается автор.
Нельзя сказать, что тут всё плохо: авторы подгружаются по мере того, как в них возникает нужда — поэтому в примере запросы перемешались с выводом print()
. Если требуется вывести тот самый список заголовков постов и для первых двух показать автора, то это приемлемый код.
Если же все посты нужны вместе с авторами, нужно использовать метод .select_related()
, который эквивалентен JOIN
в SQL. Если при вызове метода не указывать аргументы, то будут при'JOIN'ены все связанные таблицы. Если же указать имена связей, то указанные таблицы подгрузятся сразу, остальные — как обычно, по требованию. Это достаточно гибкое средство, особенно в сочетании с другими способами оптимизации:
post = Post.objects.select_related('creator').only('title', 'creator__email')[0]
# SELECT "blog_post"."id",
# "blog_post"."title",
# "blog_post"."creator_id",
# "blog_user"."id",
# "blog_user"."email"
# FROM "blog_post"
# INNER JOIN "blog_user"
# ON ("blog_post"."creator_id" = "blog_user"."id")
# LIMIT 1
# Execution time: 0.000421s [Database: default]
post.title, post.creator.email
# => ('Intro', 'bob@blogs.org')
post.body
# SELECT "blog_post"."id",
# "blog_post"."body"
# FROM "blog_post"
# WHERE "blog_post"."id" = 1
# LIMIT 21
# Execution time: 0.000161s [Database: default]
# => 'Hi, my name is Bob!'
post.creator.first_name
# SELECT "blog_user"."id",
# "blog_user"."first_name"
# FROM "blog_user"
# WHERE "blog_user"."id" = 1
# LIMIT 21
# Execution time: 0.000346s [Database: default]
# => 'Bob'
Следует обратить внимание, что первый запрос содержал только указанные поля пусть даже и в двух таблицах (плюс служебные поля вроде id
и поля для связи таблиц). При этом с точки зрения наблюдателя post
выглядит как цельный объект модели Post
, а его атрибут .creator
, как цельный объект модели User
, а все дополнительные данные прозрачно подгружаются по мере необходимости.
Вам ответят команда поддержки Хекслета или другие студенты.
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
Наши выпускники работают в компаниях:
С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.
Зарегистрируйтесь или войдите в свой аккаунт