Руслан Волшебник
Руслан Волшебник25. Januar 2019 04:50

Оптимизация запросов Django ORM

Django, ORM, GenericForeignKey, GenericRelation

Доброго времени суток.

Я как-то задавал вопросы в комментариях под этой статьей. Для проверки, стоит ли лайк у пользователя к данной статье, был дан такой ответ:

"я написал template tag, фильтр, через который делаю проверку прямо в шаблоне

@register.filter
def user_in(objects, user):
    if user.is_authenticated:
        return objects.filter(user=user).exists()
    return False

Для этого подгружаю модуль в шаблоне и проверяю наличие пользователя в queryset

{% load users_extras %}
<span class="mdi mdi-star mr-1 {% if obj.likes.all|user_in:user %}text-success{% endif %}"></span>

Плюс в том, что могу делать такую проверку для какой угодно модели данных, у которой поле пользователя называется user. И нет никакой зависимости на ModelManager".

Теперь к сути.

Я вывожу статьи в шаблоне используя цикл.

{% for article in articles %}
{% endfor %}

В моем случае, 18 итераций в цикле.

Запросов к бд в шаблоне всего навсего три.

Так вот, используя выше приведенный способ проверки на наличие лайка, на каждой итерации выполняется запрос.

У меня идет проверка на наличие лайка/дизлайка и выводится количество лайков/дизлайков для каждой статьи.

<div class="like-icon {% if article.votes.likes.all|user_in:user %} text-success {% endif %}" data-icon="like"></div>
<div class="like-count" data-count="like">
  {{ article.votes.likes.count }}
</div>

<div class="dislike-icon {% if article.votes.disikes.all|user_in:user %} text-success {% endif %}" data-icon="dislike"></div>
<div class="dislike-count" data-count="dislike">
  {{ article.votes.dislikes.count }}
</div>

В итоге получается 72 запроса. Это слишком много.

Как все это дело оптимизировать?

Рекомендуємо хостинг TIMEWEB
Рекомендуємо хостинг TIMEWEB
Stabiles Hosting des sozialen Netzwerks EVILEG. Wir empfehlen VDS-Hosting für Django-Projekte.

Magst du es? In sozialen Netzwerken teilen!

24
Evgenii Legotckoi
  • 25. Januar 2019 04:55

Добрый день!

Это можно оптимизировать через prefetch_related, сам пару недель назад пол сайта перелопатил с этой оптимизацией. К сожалению с лайками, дислайками и прочими красивостями есть такая проблема. Но полностью избавиться от большого количества запросов всё равно не выйдет. Мне нужно посмотреть исходники сайта, мне удалось местами количество запросов уменьшить с 483 до 52. Вечером подробнее отвечу.

    Руслан Волшебник
    • 25. Januar 2019 05:12
    • (bearbeitet)

    Я пробовал такой способ:

    Сначала получаю массив словарей(объектов). И отправляю их в шаблон.

    likedislikes = LikeDislike.objects.filter(content_type__model='post', object_id__in=obj_ids).values()
    

    Для вывода количества лайков и дизлайков для каждой статьи, написал simple tag, в который я отправляю массив объектов (likedislikes), id статьи (object_id) и тип голоса лайк/дизлайк(1/-1) (vote).

    @register.simple_tag
    def vote_count(likedislikes, object_id, vote):
        count = 0
        for item in likedislikes:
            if item['object_id'] == object_id and item['vote'] == vote:
                count += 1
        return count
    
    <div class="like-count" data-count="like">
      {% vote_count likedislikes article.id 1 %}
    </div>
    <div class="dislike-count" data-count="dislike">
      {% vote_count likedislikes article.id -1 %}
    </div>
    

    Для проверки на наличие лайка/дизлайка, почти тоже самое:

    simple tag

    @register.simple_tag
    def user_in(likedislikes, object_id, vote, user):
        if user.is_authenticated:
            for item in likedislikes:
                if item['object_id'] == object_id and item['user_id'] == user.id and item['vote'] == vote:
                    return ''
        return 'text-success'
    
    

    В шаблоне

    <div class="like-icon {% user_in likedislikes post.id 1 user %}" data-icon="like"></div>
    
    <div class="dislike-icon {% user_in likedislikes post.id -1 user %}" data-icon="dislike"></div>
    

    В таком случае всего 1 запрос к бд.

    Я как бы не спец, только начинающий, но мне кажется, что такой вариант не очень хорош.
    Может скажете, что вы считаете на этот счет?

      Это не поможет.

      Вы в любом случае будете делать дополнительные запросы к базе данных.

      В Django все запросы являются ленивыми и выполняются в самый последний момент, когда они уже действительно нужны.

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

      Это:

      • Select Related - когда сразу выбираются объекты, на которые есть foreign key в вашем объекте
      • Prefetch Related - когда выбираются объекты, которые имеют foreign key на ваш объект

      Также можно делать агрегацию запросов, что также может уменьшить время и количество запросов.

      Я у себя для оптимизации использовал все эти варианты. Удалось выиграть где-то в 2,5 раза по времени на запросы.

      Пример

      Есть Section, Article и Comment, которые имеют foregin key на Article. При этом Article имеет foreign key на Section. В этом случае мы отображаем список статей с информацией о разделе и списком комментариев.

      Вот пример запроса для выборки всех статей в данном случае.

      Article.object.all().select_related('section').prefetch_related('comment_set')
      

      В этом случае колиечтво запросов может уменьшиться втрое.

      Тоже самое можно для начала сделать и для лайков.

      Article.object.all().prefetch_related('votes')
      

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

        Руслан Волшебник
        • 25. Januar 2019 05:51
        • (bearbeitet)

        Вы пишете "Это не поможет." Но смотрите, было так -
        sql_74_e4nXlW1.jpeg sql_74_e4nXlW1.jpeg
        и
        CPU_for_74_s0RvimJ.jpeg CPU_for_74_s0RvimJ.jpeg
        , а после моих манипуляций, описанных выше, стало так -
        sql_4_E5GPNJt.jpeg sql_4_E5GPNJt.jpeg
        и
        CPU_for_4_I73arJB.jpeg CPU_for_4_I73arJB.jpeg
        . Может я что-то не понимаю, но запросы уменьшились. Причем в 18,5 раз)

          Evgenii Legotckoi
          • 25. Januar 2019 06:01
          • (bearbeitet)

          Вы не могли бы добавлять изображения через диалог добавления изображений? )))

          А, понятно, я не обратил внимание на выбору по массиву ids... Да, вы правы в данном случае. Просто получается, что у вас отдельно идёт массив лайков и дислайков. Вот только этот цикл вам может боком в будущем выйти

          for item in likedislikes:
              if item['object_id'] == object_id and item['user_id'] == user.id and item['vote'] == vote:
                  return ''
          

          Представьте, что объектов в likedislikes будет эдак 50 тысяч или больше. Думаю, что если это делать средствами базы данных, а не python, то такая проблема станет актуальной гораздо позже.

            Руслан Волшебник
            • 25. Januar 2019 06:45
            • (bearbeitet)

            "Вы не могли бы добавлять изображения через диалог добавления изображений? )))".

            Да, извиняюсь)

            Попробую, ещё поэкспериментировать.

              На счет prefetch_related. Да, я пользуюсь этой штукой для ForeignKey. Но для GenericForeignKey/GenericRelation, не работает. Я попробовал и у меня просто добавился ещё 1 запрос.

                хм. надо будет у себя перепроверить. Вроде бы ситуация улучшилась с использованием prefetch_related на GenericRelation. Но если я помню правильно, то вы отказались от использования GenericRelation? Может поэтому не работает?

                  Руслан Волшебник
                  • 25. Januar 2019 07:41
                  • (bearbeitet)

                  Я решил, обратно вернуть GenericRealtion, так как мне очень понравилось, что можно так легко получать все лайки/дизлайки и их количество к статьям, простыми строками)))

                  article.votes.likes()
                  article.votes.likes().count()
                  

                    Это да, а если учесть, что GenericRelation можно подключать абсолютно к любому типу контента, то вообще огонь получается.

                      Кстати, есть ещё такой вопрос. Что тогда вообще делать, если лайков миллионы? Просто интересно, как всё это дело хранит и обрабатывает тот же youtube. У них, наверное, какая-то другая система, а то обрабатывать миллиарды или даже триллионы объектов LikeDislike не реально.

                        Evgenii Legotckoi
                        • 25. Januar 2019 08:11
                        • (bearbeitet)

                        Полагаю, что они это дело кэшируют и изменяют информацию только при появлении или удалении лайка.

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

                        Но по факту, первое, что нужно будет делать - это кэшировать.

                        А если говорить об этом сайте, то на данный момент я до сих пор не заморачивался над подключением даже того же самого memchached. Некоторые оптимизации запросов, структуры отдачи контента, периодический рефакторинг, хостинг на VDS делают своё дело. В принципе сайт обладает достаточной отзывчивостью, даже при использовании скромных ресурсов без повсеместного кэша.

                          Руслан Волшебник
                          • 26. Januar 2019 06:01
                          • (bearbeitet)

                          Я начал копаться в этих агрегациях и аннотациях. У меня получилось взять количество лайков/дизлайков по отдельности.

                          a1 = Article.objects.filter(votes__vote__gt=0).annotate(like_count=Count('votes'))
                          print(a1.values('id', 'like_count'))
                          
                          a2 = Article.objects.filter(votes__vote__lt=0).annotate(dislike_count=Count('votes'))
                          print(a2.values('id', 'dislike_count'))
                          

                          Пока не понятно как получить все вместе.

                            Руслан Волшебник
                            • 27. Januar 2019 05:06
                            • (bearbeitet)

                            Добрый день!

                            Я нашел решение.

                            Вот код, который вычисляет количество лайков и дизлайков для каждой статьи и добавляет поля like_count и dislike_count к объектам article.

                            articles = Article.objects.annotate(like_count=Count('votes', filter=Q(votes__vote__gt=0))).annotate(dislike_count=Count('votes', filter=Q(votes__vote__lt=0)))
                            

                            В шаблоне

                            {{ article.like_count }}
                            {{ article.dislike_count }}
                            

                            Код, для нахождения пользователя в лайках и дизлайках. Добавляет к объектам поля like, dislike, в которых в зависимости от нахождения пользователя, будут значения 0 или 1.

                            articles = articles.annotate(like=Count('votes__user', filter=Q(votes__user=request.user) & Q(votes__vote__gt=0))).annotate(dislike=Count('votes__user',filter=Q(votes__user=request.user) & Q(votes__vote__lt=0)))
                            

                            В шаблоне

                            {% if article.like %} success {% endif %}
                            {% if article.dislike %} success {% endif %}
                            

                            Запросов к бд всего 3. Вместе они выполняются за 7-14 ms.

                            Хотелось бы узнать, что вы думаете по этому поводу. Будет ли высокая нагрузка при
                            большом количестве LikeDislike? И можно ли как-то улучшить?

                              Добрый день.

                              Минус annotate в том, что он очень медленный, лучше сделать через aggregate

                              likedislikes = obj.votes.all().aggregate(
                                  like_total=Count('id', filter=Q(vote=LikeDislike.LIKE)),
                                  like_user_in=Count('user', filter=Q(user=user, vote=LikeDislike.LIKE)),
                                  dislike_total=Count('id', filter=Q(vote=LikeDislike.DISLIKE)),
                                  dislike_user_in=Count('user', filter=Q(user=user, vote=LikeDislike.DISLIKE))
                              )
                              
                              context = {
                                  'obj': obj,
                                  'likes_count': likedislikes['like_total'],
                                  'dislikes_count': likedislikes['dislike_total'],
                                  'likes_with_user': 'text-success' if likedislikes['like_user_in'] > 0 else '',
                                  'dislikes_with_user': 'text-success' if likedislikes['dislike_user_in'] > 0 else '',
                              }
                              

                              Только заметьте,что это нормально сработает, если пользователь аутентифицирован. То есть, прежде чем делать проверку на наличие пользователя среди проголосовавших, нужно сделать проверку is_authenticated

                                В моем случае, для вывода 18 объектов, нужно внести этот код в цикл, но тогда и запросов будет 18.

                                  Evgenii Legotckoi
                                  • 29. Januar 2019 05:22
                                  • Die Antwort wurde als Lösung markiert.

                                  А здесь уже собственно начинается поиск золотой середины. Annotate методы в моём случае давали очень долгие запросы. Да, я получал один запрос, вот только длительность этого запроса была вдвое больше, чем выполнить два десятка таких агрегированных запросов. В общем, как у вас будет достаточно контента и сайт будет достаточно развит, то поиграйтесь с обоими вариантами. А там и сделаете выводы, что вам больше подходит. Один тяжёлый запрос или два десятка лёгких.

                                    Большое спасибо, теперь мне все понятно.

                                      Руслан Волшебник
                                      • 5. April 2019 07:34
                                      • (bearbeitet)

                                      Здравствуйте. Снова проблема с запросами.

                                      Вот такая картина, когда я в админке просто открываю страницу с пользователем.

                                      models.py

                                      class User(AbstractUser):
                                      
                                          def __str__(self):
                                              return self.username
                                      

                                      admin.py

                                      class AuthUserAdmin(admin.ModelAdmin):
                                          list_display = ("id", "username", "email")
                                          fields = ("username", "email", "date_joined", "last_login", "groups", "user_permissions", "is_active", "is_staff", "is_superuser")
                                          readonly_fields = ("date_joined", "last_login")
                                      

                                        Я заметил, что если сделать поле "user_permissions", только для чтения, то всё нормально(8 запросов).

                                          Так может быть. user_permissions - имеют достаточно сложную систему, поскольку определяют доступ пользователей в админке к определённым моделям. И там имеется возможность выбора тех или иных прав на доступ к моделям. Скорее всего вызывается несколько запросов.

                                          Только я не понимаю вашей проблемы в данном случае. Какой вообще смысл оптимизировать запросы там, где обычно простые пользователи вообще не имеют доступа? Вам эта попытка оптимизации не принесёт ровно никакой выгоды.

                                            Спасибо. Я вас понял. Видимо я просто уже "погнал" на этой оптимизации)

                                            И последний вопрос. Есть ли принципиальная разница добавлять в foreignkey объекта LikeDislike User или UserProfile? Мне просто удобнее UserProfile и добавлять в inlines объект LikeDislike, чтобы отображть сразу под профилем список голосов.

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

                                                Спасибо вам большое. Вы помогли мне в решении многих вопросов.

                                                  Kommentare

                                                  Nur autorisierte Benutzer können Kommentare posten.
                                                  Bitte Anmelden oder Registrieren
                                                  Letzte Kommentare
                                                  ИМ
                                                  Игорь Максимов5. Oktober 2024 07:51
                                                  Django – Lektion 064. So schreiben Sie eine Python-Markdown-Erweiterung Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
                                                  d
                                                  dblas55. Juli 2024 11:02
                                                  QML - Lektion 016. SQLite-Datenbank und das Arbeiten damit in QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
                                                  k
                                                  kmssr8. Februar 2024 18:43
                                                  Qt Linux - Lektion 001. Autorun Qt-Anwendung unter Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
                                                  Qt WinAPI - Lektion 007. Arbeiten mit ICMP-Ping in Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
                                                  EVA
                                                  EVA25. Dezember 2023 10:30
                                                  Boost - statisches Verknüpfen im CMake-Projekt unter Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
                                                  Jetzt im Forum diskutieren
                                                  J
                                                  JacobFib17. Oktober 2024 03:27
                                                  добавить qlineseries в функции Пользователь может получить любые разъяснения по интересующим вопросам, касающимся обработки его персональных данных, обратившись к Оператору с помощью электронной почты https://topdecorpro.ru…
                                                  JW
                                                  Jhon Wick1. Oktober 2024 15:52
                                                  Indian Food Restaurant In Columbus OH| Layla’s Kitchen Indian Restaurant If you're looking for a truly authentic https://www.laylaskitchenrestaurantohio.com/ , Layla’s Kitchen Indian Restaurant is your go-to destination. Located at 6152 Cleveland Ave, Colu…
                                                  КГ
                                                  Кирилл Гусарев27. September 2024 09:09
                                                  Не запускается программа на Qt: точка входа в процедуру не найдена в библиотеке DLL Написал программу на C++ Qt в Qt Creator, сбилдил Release с помощью MinGW 64-bit, бинарнику напихал dll-ки с помощью windeployqt.exe. При попытке запуска моей сбилженной программы выдаёт три оши…
                                                  F
                                                  Fynjy22. Juli 2024 04:15
                                                  при создании qml проекта Kits есть но недоступны для выбора Поставил Qt Creator 11.0.2. Qt 6.4.3 При создании проекта Qml не могу выбрать Kits, они все недоступны, хотя настроены и при создании обычного Qt Widget приложения их можно выбрать. В чем может …

                                                  Folgen Sie uns in sozialen Netzwerken