Руслан Волшебник
Jan. 25, 2019, 3:50 p.m.

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

Django, ORM, GenericForeignKey, GenericRelation

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

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

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

  1. @register.filter
  2. def user_in(objects, user):
  3. if user.is_authenticated:
  4. return objects.filter(user=user).exists()
  5. return False

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

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

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

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

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

  1. {% for article in articles %}
  2. {% endfor %}

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

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

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

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

  1. <div class="like-icon {% if article.votes.likes.all|user_in:user %} text-success {% endif %}" data-icon="like"></div>
  2. <div class="like-count" data-count="like">
  3. {{ article.votes.likes.count }}
  4. </div>
  5.  
  6. <div class="dislike-icon {% if article.votes.disikes.all|user_in:user %} text-success {% endif %}" data-icon="dislike"></div>
  7. <div class="dislike-count" data-count="dislike">
  8. {{ article.votes.dislikes.count }}
  9. </div>

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

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

2
The question is asked by the articleDjango - Tutorial 023. Like Dislike system using GenericForeignKey

Do you like it? Share on social networks!

24
Evgenii Legotckoi
  • Jan. 25, 2019, 3:55 p.m.

Добрый день!

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

    Руслан Волшебник
    • Jan. 25, 2019, 4:12 p.m.
    • (edited)

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

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

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

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

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

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

    simple tag

    1. @register.simple_tag
    2. def user_in(likedislikes, object_id, vote, user):
    3. if user.is_authenticated:
    4. for item in likedislikes:
    5. if item['object_id'] == object_id and item['user_id'] == user.id and item['vote'] == vote:
    6. return ''
    7. return 'text-success'
    8.  

    В шаблоне

    1. <div class="like-icon {% user_in likedislikes post.id 1 user %}" data-icon="like"></div>
    2.  
    3. <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. В этом случае мы отображаем список статей с информацией о разделе и списком комментариев.

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

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

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

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

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

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

        Руслан Волшебник
        • Jan. 25, 2019, 4:51 p.m.
        • (edited)

        Вы пишете "Это не поможет." Но смотрите, было так -
        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
          • Jan. 25, 2019, 5:01 p.m.
          • (edited)

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

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

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

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

            Руслан Волшебник
            • Jan. 25, 2019, 5:45 p.m.
            • (edited)

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

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

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

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

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

                  Руслан Волшебник
                  • Jan. 25, 2019, 6:41 p.m.
                  • (edited)

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

                  1. article.votes.likes()
                  2. article.votes.likes().count()

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

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

                        Evgenii Legotckoi
                        • Jan. 25, 2019, 7:11 p.m.
                        • (edited)

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

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

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

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

                          Руслан Волшебник
                          • Jan. 26, 2019, 5:01 p.m.
                          • (edited)

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

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

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

                            Руслан Волшебник
                            • Jan. 27, 2019, 4:06 p.m.
                            • (edited)

                            Добрый день!

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

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

                            1. 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)))

                            В шаблоне

                            1. {{ article.like_count }}
                            2. {{ article.dislike_count }}

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

                            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)))

                            В шаблоне

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

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

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

                              Добрый день.

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

                              1. likedislikes = obj.votes.all().aggregate(
                              2. like_total=Count('id', filter=Q(vote=LikeDislike.LIKE)),
                              3. like_user_in=Count('user', filter=Q(user=user, vote=LikeDislike.LIKE)),
                              4. dislike_total=Count('id', filter=Q(vote=LikeDislike.DISLIKE)),
                              5. dislike_user_in=Count('user', filter=Q(user=user, vote=LikeDislike.DISLIKE))
                              6. )
                              7.  
                              8. context = {
                              9. 'obj': obj,
                              10. 'likes_count': likedislikes['like_total'],
                              11. 'dislikes_count': likedislikes['dislike_total'],
                              12. 'likes_with_user': 'text-success' if likedislikes['like_user_in'] > 0 else '',
                              13. 'dislikes_with_user': 'text-success' if likedislikes['dislike_user_in'] > 0 else '',
                              14. }

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

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

                                  Evgenii Legotckoi
                                  • Jan. 29, 2019, 4:22 p.m.
                                  • The answer was marked as a solution.

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

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

                                      Руслан Волшебник
                                      • April 5, 2019, 5:34 p.m.
                                      • (edited)

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

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

                                      models.py

                                      1. class User(AbstractUser):
                                      2.  
                                      3. def __str__(self):
                                      4. return self.username

                                      admin.py

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

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

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

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

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

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

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

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

                                                  Comments

                                                  Only authorized users can post comments.
                                                  Please, Log in or Sign up
                                                  • Last comments
                                                  • AK
                                                    April 1, 2025, 11:41 a.m.
                                                    Добрый день. В данный момент работаю над проектом, где необходимо выводить звук из программы в определенное аудиоустройство (колонки, наушники, виртуальный кабель и т.д). Пишу на Qt5.12.12 поско…
                                                  • Evgenii Legotckoi
                                                    March 9, 2025, 9:02 p.m.
                                                    К сожалению, я этого подсказать не могу, поскольку у меня нет необходимости в обходе блокировок и т.д. Поэтому я и не задавался решением этой проблемы. Ну выглядит так, что вам действитель…
                                                  • VP
                                                    March 9, 2025, 4:14 p.m.
                                                    Здравствуйте! Я устанавливал Qt6 из исходников а также Qt Creator по отдельности. Все компоненты, связанные с разработкой для Android, установлены. Кроме одного... Когда пытаюсь скомпилиров…
                                                  • ИМ
                                                    Nov. 22, 2024, 9:51 p.m.
                                                    Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
                                                  • Evgenii Legotckoi
                                                    Oct. 31, 2024, 11:37 p.m.
                                                    Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup