Privacy policyContactsAbout siteOpinionsGitHubDonate
© EVILEG 2015-2018
Recommend hosting
TIMEWEB
РВ
Jan. 25, 2019, 9: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 запроса. Это слишком много.

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

18

Добрый день!

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

0
РВ

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

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

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

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

0

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

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

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

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

1
РВ

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

0

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

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

1
РВ

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

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

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

0
РВ

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

0

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

0
РВ

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

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

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

0
РВ

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

0

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

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

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

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

1
РВ

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

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

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

0
РВ

Добрый день!

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

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

0

Добрый день.

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

1
РВ

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

0

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

1
РВ

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

1

Comments

Only authorized users can post comments.
Please, Log in or Sign up
Last comments
Feb. 21, 2019, 12:51 p.m.
Евгений Легоцкой

Иногда CMake приходится перезапускать начисто, не обновляет кэш
R
Feb. 21, 2019, 12:29 p.m.
RandyGallup

Я указал данные строки, т.к. без них у меня вылетала следующая ошибка: By not providing "FindQt5Core.cmake" in CMAKE_MODULE_PATH this project has asked CMake to find a package configurat...
Feb. 21, 2019, 12:08 p.m.
BlinCT

Вот атк выглядит мой проектник, посмотрите его. cmake_minimum_required(VERSION 3.6)project(projecttimer)set(CMAKE_CXX_STANDARD 11)set(CMAKE_AUTOMOC ON)set(CMAKE_AUTORCC ON)find_packa...
Feb. 21, 2019, 12:04 p.m.
BlinCT

Смотрите, если вы используете глобально для проекта -DCMAKE_PREFIX_PATH= то вам не надо уже указывать вот эти строкиset(Qt5Core_DIR "C:/Qt/5.12.1/mingw73_64/lib/cmake/Qt5Core")set(Qt5Gui_DIR...
R
Feb. 21, 2019, 11:54 a.m.
RandyGallup

Даже не запускается. main.cpp у меня точно такой же, как в статье. CMakeLists.txt пришлось немного подправить (прикрепил ниже), т.к. не находились некоторые файлы. cmake_minimum_requi...
Now discuss on the forum
Feb. 21, 2019, 8:58 a.m.
Евгений Легоцкой

Ну у меня координаты передавались в зависимости от положения курсора мыши, а в вам по сути нужно будет аналогичным способом посылать даннные из полей ввода. Так что здесь скорее интерфес...
Feb. 20, 2019, 9:55 p.m.
Евгений Легоцкой

Не до конца понимаю сути вопроса, наверное, нужно увидеть программный код и попытку его применения, но к методам базового класса можно обращаться в наследованном классе через вызов по имени ба...
MU
Feb. 20, 2019, 3:06 p.m.
Maciej Urmański

Yes, ok I have solution! Thank you for directing me about annotate.:) Solution is: users_in = User.objects.filter(joined_users__goal=goal, joined_users__joined=True)
Feb. 20, 2019, 2:40 p.m.
Евгений Легоцкой

Думаю, что ещё можно переопределить mouseReleaseEvent(QMouseEvent* event) у QTableView, который содержит модель и немного поиграться с индексом. Если это индекс, который соответству...
Feb. 20, 2019, 10:34 a.m.
Евгений Легоцкой

Да, так тоже можно. Единственный момент в том, что lupdate не всегда понимает, к какому контексту это дело относится, и может запихать в левый контекст. В небольшом проекте это не критич...
Join us in social networks

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