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

Комментарии

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

C++ - Тест 006. Перечисления

  • Результат:10баллов,
  • Очки рейтинга-10
K
  • KiRi4
  • 7 сентября 2023 г. 7:57

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

  • Результат:41баллов,
  • Очки рейтинга-8
K
  • KiRi4
  • 7 сентября 2023 г. 7:49

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

  • Результат:66баллов,
  • Очки рейтинга-1
Последние комментарии
IscanderChe
IscanderChe13 сентября 2023 г. 9:11
Пример использования QScintilla C++ По горячим следам (с другого форума вопрос задали, пришлось в памяти освежить всё) решил дополнить. Качаем исходники с https://riverbankcomputing.com/software/qscintilla/downlo…
Evgenii Legotckoi
Evgenii Legotckoi6 сентября 2023 г. 7:18
Qt/C++ - Урок 048. QThread - работа с потоками с помощью moveToThread Разве могут взаимодействовать объекты из разных нитей как-то, кроме как через сигнал-слоты?" Могут. Выполняя оператор new , Вы выделяете под объект память в куче (heap), …
AC
Andrei Cherniaev5 сентября 2023 г. 3:37
Qt/C++ - Урок 048. QThread - работа с потоками с помощью moveToThread Я поясню свой вопрос. Выше я писал "Почему же в методе MainWindow::on_write_1_clicked() Можно обращаться к методам exampleObject_1? Разве могут взаимодействовать объекты из разных…
n
nvn31 августа 2023 г. 9:47
QML - Урок 004. Сигналы и слоты в Qt QML Здравствуйте! Прекрасный сайт, отличные статьи. Не хватает только готовых проектов для скачивания. Многих комментариев типа appCore != AppCore просто бы не было )))
NSProject
NSProject24 августа 2023 г. 13:40
Django - Урок 023. Like Dislike система с помощью GenericForeignKey Ваша ошибка связана с gettext from django.utils.translation import gettext_lazy as _ Поле должно выглядеть так vote = models.SmallIntegerField(verbose_name=_("Голос"), choices=VOTES) …
Сейчас обсуждают на форуме
IscanderChe
IscanderChe17 сентября 2023 г. 9:24
Интернационализация строк в QMessageBox Странная картина... Сделал минимально работающий пример - всё работает. Попробую на другой операционке. Может, дело в этом.
NSProject
NSProject17 сентября 2023 г. 8:49
Помогите добавить Ajax в проект В принципе ничего сложного с отправкой на сервер нет. Всё что ты хочешь отобразить на странице передаётся в шаблон и рендерится. Ты просто создаёшь файл forms.py в нём описываешь свою форму и в …
BlinCT
BlinCT15 сентября 2023 г. 12:35
Размеры полей в TreeView Всем привет. Пытаюсь сделать дерево вот такого вида Пытаюсь организовать делегат для каждой строки в дереве. ТО есть отступ какого то размера и если при открытии есть под…
IscanderChe
IscanderChe8 сентября 2023 г. 12:07
Кастомная QAbstractListModel и цвет фона, цвет текста и шрифт Похоже надо не абстрактный , а "реальный" типа QSqlTableModel Да, но не совсем. Решилось с помощью стайлшитов и setFont. Спасибо за отлик!
Evgenii Legotckoi
Evgenii Legotckoi6 сентября 2023 г. 6:35
Вопрос: Нужно ли в деструкторе удалять динамически созданные QT-объекты. Напр: Зависит от того, как эти объекты были созданы. Если вы передаёте указатель на parent объект, то не нужно, Ядро Qt само разрулит удаление, если нет, то нужно удалять вручную, иначе будет ут…

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