Evgenii Legotckoi
Evgenii Legotckoi22 октября 2019 г. 11:39

Django - Урок 049. Оптимизация производительности 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',
    ...
]

Вам также необходимо добавить URL-адреса из 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 просто съест все ваши ресурсы. Поэтому экспериментируйте только на сервере разработки.

Оптимизация

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

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

The first step in optimizing the EVILEG forum homepage

На данный момент все запросы будут отображаться в режиме отладки.

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

  • 325 queries to the database
  • spent 155 ms
  • 568 ms full page

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

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

Проводим оптимизацию

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

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

1 493 / 5 000
Результаты перевода
Как видите, ничего сложного. Именно такие запросы обычно пишут в самом начале использования Django ORM. И только потом начинаются вопросы, как оптимизировать производительность Django.

Дело в том, что исходный набор запросов, необходимый для отображения этой страницы, берет только объекты 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.

Performance Improvement Using Select_related

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

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

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

Как видите, продолжительность этого запроса составила 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 я получаю следующий результат

Using select_related and prefetch_related

В результате имеем следующее:

  • 6 queries to the database
  • spent on 26 ms
  • 148 ms full page

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

Но и это еще не все, обратите внимание, что запрос, имеющий 4 операции соединения, еще находится в районе 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 queries to the database
  • spent on 20 ms
  • 136 ms full page

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

Теперь давайте немного поиграем с запросами select_related и prefetch_related .

Additional optimization

Дочитав до этого места, вы, я думаю, убедились, что использование select_related позволяет очень здорово оптимизировать запросы к базе данных. Но есть одно НО . Некоторые проблемы могут возникнуть при использовании класса Paginator , который используется на моей странице. А дело в том, что для Paginator необходимо выполнить запрос 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 queries to the database
  • spent 14 ms
  • 141 ms full page

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

Вывод

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

Вам это нравится? Поделитесь в социальных сетях!

Руслан Волшебник
  • 24 октября 2019 г. 21:37

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

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

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

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

Evgenii Legotckoi
  • 25 октября 2019 г. 12:54

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

p
  • 25 ноября 2019 г. 20:11

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

Evgenii Legotckoi
  • 25 ноября 2019 г. 20:21

Стоило бы упомянуть про 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

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

D
  • 2 марта 2021 г. 13:48

Огромное спасибо вам за статью! Для меня стали открытием select_related и prefetch_related

Комментарии

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

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:70баллов,
  • Очки рейтинга1
РК

Qt - Тест 001. Сигналы и слоты

  • Результат:84баллов,
  • Очки рейтинга4
ВМ

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

  • Результат:80баллов,
  • Очки рейтинга4
Последние комментарии
k
kmssr9 февраля 2024 г. 0:43
Qt Linux - Урок 001. Автозапуск Qt приложения под Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
АК
Анатолий Кононенко5 февраля 2024 г. 7:50
Qt WinAPI - Урок 007. Работаем с ICMP Ping в Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVA25 декабря 2023 г. 16:30
Boost - статическая линковка в CMake проекте под Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
J
JonnyJo25 декабря 2023 г. 14:38
Boost - статическая линковка в CMake проекте под Windows Сделал всё по-как у вас, но выдаёт ошибку [build] LINK : fatal error LNK1104: не удается открыть файл "libboost_locale-vc142-mt-gd-x64-1_74.lib" Хоть убей, не могу понять в чём дел…
G
Gvozdik19 декабря 2023 г. 3:01
Qt/C++ - Урок 056. Подключение библиотеки Boost в Qt для компиляторов MinGW и MSVC Для решения твой проблемы добавь в файл .pro строчку "LIBS += -lws2_32" она решит проблему , лично мне помогло.
Сейчас обсуждают на форуме
NSProject
NSProject23 июня 2024 г. 19:48
добавить qlineseries в функции А куда собстаенно делся Евгений раз на сайте такой бордак творится?
BlinCT
BlinCT5 мая 2024 г. 11:46
Написать свой GraphsView Всем привет. В Qt есть давольно старый обьект дял работы с графиками ChartsView и есть в 6.7 новый но очень сырой и со слабым функционалом GraphsView. По этой причине я хочу написать х…
Evgenii Legotckoi
Evgenii Legotckoi2 мая 2024 г. 20:07
Мобильное приложение на C++Qt и бэкенд к нему на Django Rest Framework Добрый день. По моему мнению - да, но то, что будет касаться вызовов к функционалу Андроида, может создать огромные трудности.
IscanderChe
IscanderChe30 апреля 2024 г. 10:22
Во Flask рендер шаблона не передаётся в браузер Доброе утро! Имеется вот такой шаблон: <!doctype html><html> <head> <title>{{ title }}</title> <link rel="stylesheet" href="{{ url_…
G
Gar22 апреля 2024 г. 11:46
Clipboard Как скопировать окно целиком в clipb?

Следите за нами в социальных сетях