Руслан Волшебник
Руслан ВолшебникҚаң. 25, 2019, 4: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
Стабильный хостинг, на котором располагается социальная сеть EVILEG. Для проектов на Django рекомендуем VDS хостинг.

Ол саған ұнайды ма? Әлеуметтік желілерде бөлісіңіз!

24
Evgenii Legotckoi
  • Қаң. 25, 2019, 4:55 Т.Ж.

Добрый день!

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

    Руслан Волшебник
    • Қаң. 25, 2019, 5:12 Т.Ж.
    • (өңделген)

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

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

    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 запрос к бд.

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

      Evgenii Legotckoi
      • Қаң. 25, 2019, 5:23 Т.Ж.

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

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

      В 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, 2019, 5:51 Т.Ж.
        • (өңделген)

        Вы пишете "Это не поможет." Но смотрите, было так -
        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, 2019, 6:01 Т.Ж.
          • (өңделген)

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

          А, понятно, я не обратил внимание на выбору по массиву 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, 2019, 6:45 Т.Ж.
            • (өңделген)

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

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

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

              Руслан Волшебник
              • Қаң. 25, 2019, 7:25 Т.Ж.

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

                Evgenii Legotckoi
                • Қаң. 25, 2019, 7:31 Т.Ж.

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

                  Руслан Волшебник
                  • Қаң. 25, 2019, 7:41 Т.Ж.
                  • (өңделген)

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

                  article.votes.likes()
                  article.votes.likes().count()
                  
                    Evgenii Legotckoi
                    • Қаң. 25, 2019, 7:46 Т.Ж.

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

                      Руслан Волшебник
                      • Қаң. 25, 2019, 8:02 Т.Ж.

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

                        Evgenii Legotckoi
                        • Қаң. 25, 2019, 8:11 Т.Ж.
                        • (өңделген)

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

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

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

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

                          Руслан Волшебник
                          • Қаң. 26, 2019, 6:01 Т.Ж.
                          • (өңделген)

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

                          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, 2019, 5:06 Т.Ж.
                            • (өңделген)

                            Добрый день!

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

                            Вот код, который вычисляет количество лайков и дизлайков для каждой статьи и добавляет поля 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? И можно ли как-то улучшить?

                              Evgenii Legotckoi
                              • Қаң. 28, 2019, 2:21 Т.Ж.

                              Добрый день.

                              Минус 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

                                Руслан Волшебник
                                • Қаң. 29, 2019, 5:15 Т.Ж.

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

                                  Evgenii Legotckoi
                                  • Қаң. 29, 2019, 5:22 Т.Ж.
                                  • Жауап шешім ретінде белгіленді.

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

                                    Руслан Волшебник
                                    • Қаң. 29, 2019, 5:25 Т.Ж.

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

                                      Руслан Волшебник
                                      • Сәуір 5, 2019, 7:34 Т.Ж.
                                      • (өңделген)

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

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

                                      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")
                                      
                                        Руслан Волшебник
                                        • Сәуір 5, 2019, 7:54 Т.Ж.

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

                                          Evgenii Legotckoi
                                          • Сәуір 5, 2019, 9:09 Т.Ж.

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

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

                                            Руслан Волшебник
                                            • Сәуір 5, 2019, 9:27 Т.Ж.
                                            • (өңделген)

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

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

                                              Evgenii Legotckoi
                                              • Сәуір 5, 2019, 9:29 Т.Ж.

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

                                                Руслан Волшебник
                                                • Сәуір 5, 2019, 9:44 Т.Ж.

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

                                                  Пікірлер

                                                  Тек рұқсаты бар пайдаланушылар ғана пікір қалдыра алады.
                                                  Кіріңіз немесе Тіркеліңіз
                                                  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 LegotckoiҚаз. 31, 2024, 2:37 Т.Қ.
                                                  Django - Сабақ 064. Python Markdown кеңейтімін қалай жазуға болады Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
                                                  A
                                                  ALO1ZEҚаз. 19, 2024, 8:19 Т.Ж.
                                                  Qt Creator көмегімен fb3 файл оқу құралы Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
                                                  ИМ
                                                  Игорь МаксимовҚаз. 5, 2024, 7:51 Т.Ж.
                                                  Django - Сабақ 064. Python Markdown кеңейтімін қалай жазуға болады Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
                                                  d
                                                  dblas5Шілде 5, 2024, 11:02 Т.Ж.
                                                  QML - Сабақ 016. SQLite деректер қоры және онымен QML Qt-та жұмыс істеу Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
                                                  Енді форумда талқылаңыз
                                                  m
                                                  moogoҚар. 22, 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 LegotckoiМаусым 24, 2024, 3:11 Т.Қ.
                                                  добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
                                                  t
                                                  tonypeachey1Қар. 15, 2024, 6:04 Т.Ж.
                                                  google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
                                                  NSProject
                                                  NSProjectМаусым 4, 2022, 3:49 Т.Ж.
                                                  Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…

                                                  Бізді әлеуметтік желілерде бақылаңыз