- Explain
- Ограничение состава загружаемых данных
- Ранняя загрузка связанных данных
- Самостоятельная работа
Как и любое средство повышения уровня абстракции, ORM может выдавать не самые эффективные запросы в некоторых случаях. К сожалению, по-другому быть не может: тонко настроенные SQL-запросы находятся на существенно более низком уровне. В Django ORM можно переписывать код запроса руками, но есть ли в этом необходимость? Вовсе нет.
Для начала прочтем отрывок из документации к фреймворку:
- Сначала измерьте. У любого QuerySet можно запросить описание предполагаемых запросов с помощью вызова метода
.explain()
. Тут пригодится умение читать SQL и понимать то, о чем рассказывает черезexplain
ваша СУБД - Добавьте индексы. SQL explain часто может сказать, что в каком-то месте делается "full scan" то есть полный перебор, а это означает, что где-то не хватает индексов. Стоит подумать о том, чтобы оные добавить. Подробности можно почитать в документации
- Делайте как можно больше работы силами СУБД. Аннотируйте, агрегируйте. СУБД знает, как это оптимизировать и как кэшировать результаты.
- В крайнем случае пишите необходимый минимум SQL.
- Знайте, как работают QuerySets и помните:
- Насколько QuerySets ленив — он не делает лишних запросов, пока вы не попросите
- В какой момент QuerySet вычисляется (финализируется)
- Каким образом данные представлены в памяти
- Как QuerySet кеширует результаты запросов и подзапросов
Следует помнить, что многие ситуации разрешаются задолго до того, как вы дойдете до написания SQL.
Explain
Один из ключевых приемов для анализа запросов это функция EXPLAIN
. Он показывает, как база данных планирует выполнить ваш запрос. Он раскрывает такие важные детали, как использование индексов, порядок соединения таблиц и предполагаемую стоимость операций. В Django метод .explain()
предоставляет удобный доступ к этой функциональности, помогая понять, почему некоторые запросы могут работать медленнее других и как их можно оптимизировать.
from django.db.models import Count, Q
from django.db import connection
# Создаем сложный запрос для анализа
complex_query = Post.objects.annotate(
comment_count=Count('comments'),
tag_count=Count('tags')
).filter(
Q(comment_count__gte=10) |
Q(tag_count__gte=5)
).select_related('author')
print(complex_query.explain())
"""
-> Hash Join (cost=35.50..60.53 rows=100 width=40)
-> Seq Scan on blog_post (cost=0.00..25.00 rows=100 width=20)
-> Hash (cost=20.50..20.50 rows=50 width=20)
-> Index Scan using blog_tags_pkey on blog_tags
"""
EXPLAIN
выведет структуру запроса конкретной базы данных. Выше пример для Postgres, в другой базе вывод будет своим.
Ограничение состава загружаемых данных
Довольно часто нам не требуются все поля модели — нужна всего пара полей из нескольких десятков. Тут пригодится метод .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 выводит посты блога в виде списка диалогов, причем выводит тело только для пары первых пунктов. Это очень лаконичное решение с точки зрения кода, которое еще и не слишком нагружает базу.
Ранняя загрузка связанных данных
Проблема N+1 запросов - это классическая ловушка производительности при работе с ORM. Она возникает, когда код сначала делает один запрос для получения списка объектов, а затем для каждого объекта выполняет дополнительный запрос для получения связанных данных.
Например, мы открываем список из 100 постов блога, и для каждого поста нужно показать имя автора. Наивный подход приведет к тому, что сначала будет выполнен один запрос для получения постов, а затем для каждого поста будет выполнен отдельный запрос для получения информации об авторе. В итоге получится 101 запрос к базе данных (1 + 100), отсюда и название "N+1".
Вот как это выглядит в коде:
posts = Post.objects.all() # 1 запрос
for post in posts:
author_name = post.author.name # +N запросов
Здесь один запрос получает все посты, а затем для каждого поста запрашивается автор.
Нельзя сказать, что тут все плохо. Авторы подгружаются по мере необходимости, если мы хотим вывести список заголовков постов и показать автора первых двух постов, то это приемлемый код.
Если же все посты нужны вместе с авторами, нужно использовать метод .select_related()
, который эквивалентен JOIN
в SQL. Если при вызове метода не указывать аргументы, то будут присоединены все связанные таблицы. Если же указать имена связей, то указанные таблицы подгрузятся сразу, остальные — как обычно, по требованию. Это достаточно гибкое средство, особенно в сочетании с другими способами оптимизации:
post = Post.objects.select_related('author').only('title', 'author__email')[0]
print((post.title, post.author.email)) # => ('Intro', 'bob@blogs.org')
print(post.body) # => 'Hi, my name is Bob!'
print(post.author.first_name) # => 'Bob'
Обратите внимание, что первый запрос содержал только указанные поля — пусть даже и в двух таблицах — а также служебные поля вроде id
и поля для связи таблиц. При этом с точки зрения наблюдателя post
выглядит как цельный объект модели Post
, а его атрибут .author
— как цельный объект модели User
. Все дополнительные данные прозрачно подгружаются по мере необходимости.
Кроме select_related()
, в Django есть еще один похожий метод — prefetch_related()
. С помощью обоих методов можно работать со связанными объектами, но немного в разных сценариях:
Метод
select_related()
используется с отношением один ко многим. Например, такое отношение есть между автором и постом в блоге, потому что каждый пост написан одним автором. Чтобы получить информацию об авторе каждого поста, можно добавитьselect_related()
в тот же запрос к базе данных. Методselect_related()
работает через выполнениеSQL JOIN
и включение полей связанного объекта в операторSELECT
Метод
prefetch_related()
используется с отношением многие ко многим. Наглядный пример — пост в блоге, у которого может быть много комментариев. Методprefetch_related()
выполняет отдельные поиски для каждого отношения и выполняет объединение уже в Python. Так мы заранее извлекаем объекты «многие ко многим» и «многие к одному», что невозможно сделать черезselect_related()
class Post(models.Model):
title = models.CharField(max_length=200)
tags = models.ManyToManyField('Tag')
author = models.ForeignKey('Author', on_delete=models.CASCADE)
class Comment(models.Model):
post = models.ForeignKey(Post, related_name='comments')
text = models.TextField()
# загрузит все связанные данные за 3 запроса
posts = Post.objects.prefetch_related(
'comments', # загрузит все комментарии
'tags', # загрузит все теги
'comments__author' # загрузит авторов комментариев
).select_related('author') # загрузит автора поста через JOIN
Оба метода помогают избежать проблемы N+1, когда итерация по связанным объектам вызывает дополнительный запрос для каждого объекта. Но все таки один метод может быть более подходящим, чем другой — все зависит от типа отношения и конкретного случая.
Проще говоря, оба метода позволяют сказать: «Я получаю данные и знаю, что еще мне понадобятся эти другие связанные данные, поэтому получи все сразу, а не ходи туда-сюда в базу данных».
Самостоятельная работа
- На примере моделей из учебного проекта постройте несколько сложных запросов и понаблюдайте, какие запросы и в какой момент делает ORM.
- Попробуйте пооптимизировать запросы с помощью описанных в уроке средств.

Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.