Jan. 25, 2019, 3:50 a.m.

Оптимизация запросов 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 запроса. Это слишком много.

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

Virtual hosting with 10 percent discount
Virtual hosting with 10 percent discount
EVILEG offers reliable hosting with a 10% discount for virtual hosting and 5% for VPS
24

Добрый день!

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

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

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

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

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

Вы пишете "Это не поможет." Но смотрите, было так -
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 раз)

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

А, понятно, я не обратил внимание на выбору по массиву 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, то такая проблема станет актуальной гораздо позже.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Добрый день!

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

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

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

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

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

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

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, чтобы отображть сразу под профилем список голосов.

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

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

Comments

Only authorized users can post comments.
Please, Log in or Sign up
D
Aug. 16, 2019, 11:58 a.m.
Damir

C++ - Тест 003. Условия и циклы

  • Result:92points,
  • Rating points8
D
Aug. 16, 2019, 11:46 a.m.
Damir

C++ - Test 005. Structures and Classes

  • Result:75points,
  • Rating points2
u
Aug. 14, 2019, 1:55 p.m.
unrealproro

C++ - Test 005. Structures and Classes

  • Result:83points,
  • Rating points4
Last comments
Aug. 19, 2019, 6:41 a.m.
Andrej Jankovich

это проблема дистрибутива, попробуйте установить через пакетный менеджер snap Суть проблемы: libQt5Core которая лежит в дистрибутиве требует версию glibc >= 2.25 у вас видимо …
b
Aug. 18, 2019, 5:09 a.m.
bbb116

cqtdeployer /home/aleks/CQtDeployer/bin/cqtdeployer: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.25' not found (required by /home/aleks/CQtDeployer/lib/libQt5Core.so.5) linux mint …
D
Aug. 17, 2019, 8:04 a.m.
Damir

github ChekableTView Правой групповая смена значения при перетаскивании левой как обычно.
Aug. 16, 2019, 12:03 p.m.
Evgenij Legotskoj

Потому, что в минуте 60 секунд
Aug. 16, 2019, 11:16 a.m.
Dmitrij

а почему делитель 60000, а не 1000?
Now discuss on the forum
Aug. 19, 2019, 10:29 a.m.
Aleksej Vnukov

я в дороге, по тому примеру что в есть выше вам чтоб заработало нужно примерно следующее ListModel{ id: list_model } в ListView добавить model:list_model там где кноп…
Aug. 19, 2019, 8:47 a.m.
Pavel K.

bool YourClass::chekIfEmpty(const QString& table) { return getCount(table) == 0;}int YourClass::getCount(const QString& table, const QString& where) { QString command =…
Aug. 19, 2019, 8:29 a.m.
Pavel K.

посмотрите тут , практически тоже самое делал(пробегал по документу). А так в принципе : QFile file(Path);if(file.exist){ file.open(QIODevice::WriteOnly); // ReadWrite for…
Aug. 19, 2019, 2:39 a.m.
grig_p

Здравствуйте! Хотелось бы в своем приложении сделать цетрализованную обработку исключительных ситуаций для их логирования. Переопределил метод notify(): static const int EC_MAINLOOP…
Aug. 15, 2019, 2:19 a.m.
Mihailll

Плюсы и qml отличаются, с++ логичней
Looking for a Job?
14,000.00 руб. - 40,000.00 руб.
Разработчик Qt
Annino, Moscow Oblast, Russia
5,000.00 руб. - 15,000.00 руб.
Дизайнер
Moskovskiy, Moscow, Russia
25,000.00 руб. - 30,000.00 руб.
Разработчик Qt/C++
Barnaul, Altai Krai, Russia

For registered users on the site there is a minimum amount of advertising

EVILEG
About
Services
© EVILEG 2015-2019
Recommend hosting TIMEWEB