Django - Урок 049. Оптимизация производительности Django на примере боевого проекта

django-silk, performance, Django

Последнее время я посвятил много времени оптимизации сайта и теперь хотелось бы рассказать об этом.
В данной статье будет объяснено использование методов select_related и prefetch_related в QuerySet, а также их различие. Также постараюсь объяснить почему Django считают медленным, и почему это всё-таки не так. Конечно Django по многим статьям медленнее, чем тот же Flask, но в тоже время в большинстве проектов проблема заключается не в самом Django, а скорее а в отсутствии опттимизации запросов к базе данных.

Поэтому давайте оптимизируем страницу форума сайта EVILEG . А поможет нам в этом батарейка Django Silk, которая служит для измерения количества запросов к базе данных, а также измерения их длительности.

Установка и настройка Django Silk

Устанавливаем Django Silk

pip install django-silk

Добавляем его в INSTALLED_APPS

INSTALLED_APPS = [
    ...
    'silk'
]

а также добавляем MIDDLEWARE

MIDDLEWARE = [
    ...
    'silk.middleware.SilkyMiddleware',
    ...
]

Ещё на понадобится добавить urls из django-silk, чтобы можно было просматривать статистику запросов.

from django.urls import path, include

urlpatterns = [
    path('silk/', include('silk.urls', namespace='silk'))
]

И последним шагом применяем миграцию django-silk

python manage.py migrate

Примечание к Django Silk

Не используйте Django Silk на боевом сервере. По крайней мере с такими настройками, как показаны в этой статье. Если у вас есть уже хорошая посещаемость на сайте, например 1400 человек в сутки, то с такими настройками Django Silk просто съест все ваши ресурсы. Поэтому экспериментируйте только на development сервере.

Работа с оптимизацией

Для начала посмотрим, насколько всё плохо с запросами к базе данных на главной странице форума. Для ясности посмотрим, как выглядит данная страница

Для этого нам нужен всего лишь загрузить интересующую нас страницу и посмотреть статистику запроса в Django Silk.

Первый шаг оптимизации главной страницы форума сайта EVILEG

В данный момент все запросы будут показаны в дебаг режиме.

Ситуация получается удручающая, поскольку на загрузку главной страницы форума приходится:

  • 325 запросов к базе данных
  • на которые тратится 155 мс
  • 568 мс на всю страницу

Это очень большие расходы по времени, тем более, что потом все необходимые данные нужно ещё загрузить в объекты, а здесь уже всёбудет зависеть от конкретной реализации класса поля Field.

Расход ресурсов получается огромным. Полагаю, что это одна из причин, почему многие считают Django медленным, но на самом деле они просто не разобрались, как настроить и оптимизировать запросы к базе данных.

Выполняем оптимизацию

Посмотрим, как выглядит изначальный QuerySet для главной страницы форума.

def get_queryset(self):
    return ForumTopic.objects.all()

Как видите ничего сложного. Именно такие запросы как правило и пишут в самом начале использования Django ORM. А уже потом начинаются вопросы, как оптимизировать производительность Django.

Дело в том, что первоначальный queryset, который требуется для отображения данной страницы забирает из базы данных только объекты ForumTopic , но не забирает остальные объекты, которые добавлены в ForeignKey поля модели данных ForumTopic . Поэтому Django вынужден автоматически подгружать все необъодимые объекты тогда, когда они требуются. Но программист знает, что потребуется для каждой отдельной страницы и может указать Django все необъодимые объекты, которые необходимо забрать заранее одним запросов. Давайте сделаем это с помощью select_related.

select_related

Данный метод позволяет забирать в одном запросе ещё и дополнительные объекты из других таблиц. Это позволит объединить множество запросов в один и ускорить выборку, а также уменьшить накладные расходы на подключение к базе данных, посокольку количество подключений уже очень сильно уменьшится.

Давайте попробуем выбрать некоторые данные в одном запросе с помощью selected_related . Я знаю, что для моей модели ForumTopic в качестве related можно выбрать следующие поля:

  • article - статья, к которой относится вопрос на форуме
  • answer - ответ, сообщение, которое было помечено как ответ в теме форума
  • section - раздел, в котором был задан вопрос
  • user - пользователь, который задал данный вопрос

Изначальный запрос к базе данных можно модифицировать следующим образом:

def get_queryset(self, **kwargs):
    return ForumTopic.objects.all().select_related('article', 'answer', 'section', 'user')

После чего посмотрим на результат в Django Silk

Улучшение производительности с использованием select_related

Ситуация с количеством запросов уже стала лучше

  • 256 запросов к базе данных
  • на которые тратится 131 мс
  • 444 мс на всю страницу

На следующем рисунке приведена строчка с новым запросом, который имеет 4 join операции.

Как видите, длительность этого запроса составила 19,225 мс .

Уже хороший результат. Но я точно знаю, что это не предел. Дело в том, что структура главной страницы форума достаточно сложная, и на ней показано количество сообщений в каждом топике, последнее сообщение, ссылка на ответ решения, а также запрос на профиль пользователя для ответов. И вот здесь приходить очередь prefetch_related метода.

prefetch_related

prefetch_related отличается тем, что позволяет подгрузить не только объекты, которые используются в ForeignKey полях модели, но и те объекты, модели которые имеют поле ForeignKey на модель, кооторая участвует в основном запросе к базе данных. То есть можно подгрузить сообщения в топике отдельным запросом. В данном ситуации я хочу подгрузить следующие поля.

  • comments - это сообщения в топике, модель ForumPost
  • comments__user - внешний ключ на пользователя, который оставил сообщение
  • answer___parent - внешний ключ на ForumTopic, у ответа, который был помеченорешением топика. Теоретически можно было бы забрать данный объект через select_related , но структура запроса стала очень сложной, что не позволило бы эффективно использовать select_related . Да, да использование этого метода должно быть разумным. Производительность, конечно, улучшается, но иногда некоторые данные лучше забирать отдельным запросом.

Тогда запрос к базе данных уже выглядит так:

def get_queryset(self, **kwargs):
    return ForumTopic.objects.all().select_related('article', 'answer', 'section', 'user').prefetch_related(
        'comments', 'comments__user', 'answer___parent'
    )

А в Django Silk я получаю следующий результат

Использование select_related и prefetch_related

В итоге имеем следующее:

  • 6 запросов к базе данных
  • на которые тратится 26 мс
  • 148 мс на всю страницу

Это просто отличный результат, которого можно достичь. При этом для пользователя по ощущения страница уже начинает загружаться очень быстро.

Но это ещё не всё, заметьте что запрос, которые имет 4 join операции по прежнему находится в районе 17-20 мс. Можем ли мы с этим что-то сделать? Конечно можем, и для этого нам понадобится использование метода only .

only

Метод only позволяет забирать только те столбцы, которые нам нужны для отображения страницы. Но в данном случае нужно будет учесть всё столбцы, которые потребуются, иначе каждый упущенный столбец Django будет забирать отдельным запросом.

Поэтому я написал следующий запрос к базе данных

def get_queryset(self, **kwargs):
    return ForumTopic.objects.all().select_related('article', 'answer', 'section', 'user').prefetch_related(
        'comments', 'comments__user', 'answer___parent'
    ).only(
        'user__first_name', 'user__last_name', 'section__title', 'section__title_ru', 'article__title',
        'article__title_ru'
    )

И получил следующий результат

  • 6 запросов к базе данных
  • на которые тратится 20 мс
  • 136 мс на всю страницу

Конечно, я привожу лучшие возможные результаты, поскольку всегда имеются некоторые колебания в измерениях, но по данному скриншоту видно, что длительность основного запроса снизилась с 17-19 мс до 11-13 мс . Помимо прочего выборка только нужных полей снижает и потребление памяти, если из базы данных забираются например очень крупные куски текстовых данных, которые при этом не используются в рендеринге страницы.

А теперь давайте немного поиграемся с выполнением запросов select_related и prefetch_related

Дополнительная оптимизация

Дочитав до этого момента, Вы, я думаю, убедились, что использование select_related позволяет очень классно оптимизировать запросы к базе данных. Но есть одно НО . Некоторые проблемы могут возникнуть в использованием класса Paginator , который используется у меня на данной странице. И дело в том, что для Paginatorа необходимо выполнять запрос count, чтобы подсчитать правильное количество страниц. А если запрос будет очень сложным, то и длительность запроса count может оказаться довольно большой и соразмерной с выполнением обычного запроса. Поэтому важным условием может быть написание быстрого и эффектинвого основного запроса, а все остальные объекты лучше будет подгружать с помощью prefetch_related . То есть у вас может быть ситуация, когда лучше выполнить ещё пару дополнительных запросов, чем перегружать join операциями основной запрос.

И вот такой запрос к ORM я написал для данной страницы

def get_queryset(self, **kwargs):
    return ForumTopic.objects.all().select_related('answer').prefetch_related(
        Prefetch('article', queryset=Article.objects.all().only('title', 'title_ru')),
        Prefetch('section', queryset=ForumSection.objects.all().only('slug', 'title', 'title_ru')),
        Prefetch('user', queryset=User.objects.all().only('username', 'first_name', 'last_name')),
        Prefetch('comments', queryset=ForumPost.objects.all().select_related('user').only(
            'user__username', 'user__first_name', 'user__last_name', '_parent_id'
        )),
        Prefetch('answer___parent', queryset=ForumTopic.objects.all().only('id'))
    ).only(
        'title', 'user_id', 'section_id', 'article_id', 'answer___parent_id', 'pub_date', 'lastmod', 'attachment'
    )

В тоже время я получил следующий результат по производительности

  • 8 запросов к базе данных
  • на которые тратится 14 мс
  • 141 мс на всю страницу

Конечно, Вы можете сказать, что в данном случае здесь не очень-то большой выигрыш. Тем более, что общая скорость загрузки даже немного упала (5 мс), а запросов к базе данных стало на 2 больше, но в тоже время я получил прирорст производительности запросов на 42 процента , а это уже кое-чего стоит. Таким образом, если у вас на сайте имеются очень длительные запросы, которые используются в пагинации и имеют большое количество join операций, то возможно стоит переписать использование select_related на prefetch_related . Это на самом деле может помочь сделать ваш сайт на Django значительно быстрее.

Заключение

  • Используйте select_related , чтобы одновременно с основным запросом выбирать и соответствующие поля из других таблиц
  • Используйте prefetch_related , чтобы дополнительно подгружать одним запросом все объекты других моделей, которые имеют ForeignKey на ваш основной queryset
  • Используйте only , чтобы ограничить забираемые столбцы, это также ускорит запросы и уменьшит потребление памяти
  • Если вы используете Paginator , то удостоверьтесь, что основной запрос не генерирует очень тяжёлый запрос count , иначе, возможно, что стоит часть select_related запросов подгружать как prefetch_related
Рекомендуем хостинг TIMEWEB
Рекомендуем хостинг TIMEWEB
Стабильный хостинг, на котором располагается социальная сеть EVILEG. Для проектов на Django рекомендуем VDS хостинг.
- блог компании
Поддержать автора Donate

Спасибо. Хорошая статья.

Я нашёл 2 опечатки. Выделил жирным.

prefetch_related
prefetch_related отличается тем, что позволяет подгрузить не только объекты, которые используются в ForeignKey полях модели, но и те объекты, модели...
Должно вроде быть "но и те ".

Дополнительная оптимизация
...То есть у вас может быть ситуация, когда лучше выполнить ещё пару дополнительных запросов, через перегружать join операциями основной запрос.
Тут видимо имелось ввиду чем .

Спасибо, поправил

p

Стоило бы упомянуть про Prefetch объекты со специально сформированными querysetами. Про кеширование. Помимо only есть defer. В некоторых случаях в drf можно автоматически делать select/prefetch_related. И запросы можно смотреть в django_debug_toolbar или в shell_plus --print-sql

Стоило бы упомянуть про Prefetch объекты со специально сформированными querysetами

Вы про это?

def get_queryset(self, **kwargs):
    return ForumTopic.objects.all().select_related('answer').prefetch_related(
        Prefetch('article', queryset=Article.objects.all().only('title', 'title_ru')),
        Prefetch('section', queryset=ForumSection.objects.all().only('slug', 'title', 'title_ru')),
        Prefetch('user', queryset=User.objects.all().only('username', 'first_name', 'last_name')),
        Prefetch('comments', queryset=ForumPost.objects.all().select_related('user').only(
            'user__username', 'user__first_name', 'user__last_name', '_parent_id'
        )),
        Prefetch('answer___parent', queryset=ForumTopic.objects.all().only('id'))
    ).only(
        'title', 'user_id', 'section_id', 'article_id', 'answer___parent_id', 'pub_date', 'lastmod', 'attachment'
    )

В некоторых случаях в drf можно автоматически делать select/prefetch_related

Для drf можно сделать отдельную статью, я вообще не рассматривал в данной статье drf

И запросы можно смотреть в django_debug_toolbar или в shell_plus --print-sql

В качестве альтернативы

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь
Donate

Здравствуйте, уважаемые пользователи EVILEG !!!

Если сайт вам помог, то поддержите разработку сайта финансово, пожалуйста.

Вы можете сделать это следующими способами:

Спасибо, Евгений Легоцкой

ДК
16 января 2020 г. 3:19
Дмитрий Корягин

C++ - Тест 001. Первая программа и типы данных

  • Результат:73баллов,
  • Очки рейтинга1
ЛЗ
16 января 2020 г. 3:03
Лилия Зиганурова

C++ - Тест 005. Структуры и Классы

  • Результат:50баллов,
  • Очки рейтинга-4
p
13 января 2020 г. 16:59
popkadurak

C++ - Тест 002. Константы

  • Результат:100баллов,
  • Очки рейтинга10
Последние комментарии
17 января 2020 г. 2:31
Андрей Янкович

Выглядит как ошибка библиотеки. Расскажите подробно на какой платформе вы собираете проект (MinGW или MSVC) их версии и версии Qt.
D
16 января 2020 г. 12:06
DENIZ1819

Доброго времени суток, не подскажите, что делать в данной ситуации, после того, как я сделал все вышеуказанные инструкции для подключения библиотеки к проекту?
14 января 2020 г. 5:33
Евгений Легоцкой

Рекомендую Wt, достаточно мощная вещь. Этот фреймворк может использоваться для написания сайтов на C++, либо можно использовать только отдельный компоненты, например только ORM. Но я не знаю, ка…
a
14 января 2020 г. 5:29
ayb

Спасибо за инфу. Поиск качественной ORM привел меня только к sqlite_orm, но не подходит из-за необходимости полноценной поддержки c++14. Про framework Wt не слышал, спасибо за наводку.
14 января 2020 г. 2:50
Евгений Легоцкой

Вы заблуждаетесь. Любая нормальная ORM позволяет выполнение сырых SQL запросов. А если хорошо разобраться в работе моделей данных в Qt, то не составит труда использовать ORM вместе с Qt, ту же с…
Сейчас обсуждают на форуме
VZ
18 января 2020 г. 7:25
Vladimir Zhitkovsky

В приложении есть страницы с контролами. в с++ я заполняю структуры ассоциированные с контролами в qml. затем генерю сигнал о том, что все данные готовы и в qml по этому сигналу заполняю контрол…
18 января 2020 г. 7:12
Ruslan Polupan

Строку host разкоментировать и указать адрес сервера [listener];host=192.168.0.100port=8080minThreads=4maxThreads=100cleanupInterval=60000readTimeout=60000maxRequestSize=16000maxMulti…
17 января 2020 г. 2:20
Intruder

Александр, доброго дня! Я тоже только учусь и поэтому мой код может быть не совершенен. За отклик большое спасибо.
L
16 января 2020 г. 20:14
LesLype

Oct Products Similiar To Lasix Kamagra Now.Co.Uk Sky Pharmacy Canada [url=http://cialibuy.com]Buy Cialis[/url] Viagra Ricetta Ripetibile
16 января 2020 г. 18:05
Алексей Внуков

в лоадер вроде как нельзя передать значение при загрузке, я не нашел такой возможности, через стек без проблем. если использую лоадер - я передаю в С++ нужные параметры, а потом при загрузке стр…
EVILEG
О нас
Услуги
© EVILEG 2015-2019
Рекомендует хостинг TIMEWEB