Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Эффективное использование ORM Python: Django ORM

ORM, как и любое средство повышения уровня абстракции, имеет свойство выдавать не самые эффективные запросы в некоторых случаях. По-другому, увы, и не может быть: тонко настроенные SQL-запросы находятся на существенно более низком уровне. Но так ли необходимо переписывать код запроса руками — а Django ORM позволяет такое делать — сразу же как проявится "узкое место"? Вовсе нет!

Вот что говорит документация к фреймворку (дана выдержка):

  1. Сначала измерьте. У любого QuerySet можно запросить описание предполагаемых запросов с помощью вызова метода .explain(). Тут пригодится умение читать SQL и понимать то, о чём рассказывает через explain ваша СУБД.
  2. Добавьте индексов. SQL explain часто может сказать, что в каком-то месте делается "full scan" то есть полный перебор, а это означает, что где-то не хватает индексов. Стоит подумать о том, чтобы оные добавить. Подробности можно почитать в документации.
  3. Знайте, как работают QuerySets и помните
  • что QuerySets ленив, то есть не делает лишних запросов, пока вы не попросите
  • когда QuerySet вычисляется (финализируется)
  • как данные представлены в памяти, как QuerySet кеширует результаты запросов и подзапросов.
  1. Делайте как можно больше работы силами СУБД. Аннотируйте, агрегируйте. СУБД знает, как это оптимизировать и как кэшировать результаты.
  2. В крайнем случае пишите необходимый минимум SQL.

Следует помнить, что многие ситуации разрешаются задолго до того, как вы дойдёте до написания 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, которая выводит посты блога в виде списка диалогов и только для пары первых выводит тело, будет и с точки зрения кода лаконичной и не слишком нагрузит базу!

Самостоятельная работа

  1. На примере моделей из учебного проекта постройте несколько сложных запросов и понаблюдайте, какие запросы и в какой момент делает ORM.
  2. Попробуйте пооптимизировать запросы с помощью описанных в уроке средств.

Ранняя загрузка связанных данных

Если написать такой цикл:

>>> 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, а все дополнительные данные прозрачно подгружаются по мере необходимости.


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

  • задайте вопрос. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
  • расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
  • изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.
Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
900
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 120 курсов, 2000+ часов теории
  • 900 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Python-разработчик
Профессия
Разработка веб-приложений на Django
1 декабря 8 месяцев

Используйте Хекслет по максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг»