Evgenii Legotckoi
Evgenii Legotckoi22 октября 2019 г. 1: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 г. 11:37

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

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

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

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

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

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

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

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

Evgenii Legotckoi
  • 25 ноября 2019 г. 9: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 г. 2:48

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

Комментарии

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

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

  • Результат:50баллов,
  • Очки рейтинга-4
m
  • molni99
  • 26 октября 2024 г. 1:37

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

  • Результат:80баллов,
  • Очки рейтинга4
m
  • molni99
  • 26 октября 2024 г. 1:29

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

  • Результат:20баллов,
  • Очки рейтинга-10
Последние комментарии
ИМ
Игорь Максимов22 ноября 2024 г. 11:51
Django - Урок 017. Кастомизированная страница авторизации на Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii Legotckoi31 октября 2024 г. 14:37
Django - Урок 064. Как написать расширение для Python Markdown Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZE19 октября 2024 г. 8:19
Читалка fb3-файлов на Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь Максимов5 октября 2024 г. 7:51
Django - Урок 064. Как написать расширение для Python Markdown Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas55 июля 2024 г. 11:02
QML - Урок 016. База данных SQLite и работа с ней в QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Сейчас обсуждают на форуме
m
moogo22 ноября 2024 г. 7:17
Mosquito Spray System Effective Mosquito Systems for Backyard | Eco-Friendly Misting Control Device & Repellent Spray - Moogo ; Upgrade your backyard with our mosquito-repellent device! Our misters conce…
Evgenii Legotckoi
Evgenii Legotckoi24 июня 2024 г. 15:11
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey115 ноября 2024 г. 6:04
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProject4 июня 2022 г. 3:49
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…

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