Анотация после фильтра (freeze queryset) Django
В процессе аннотирования модели возникает необходимость отфильтровать готовый список. Но значение аннотации "rank"
после filter()
становится «1», так как в ней только один элемент. Без фильтрации кверисета все работает нормально
request_user = (
MainUser.objects.select_related("balance_account")
.annotate(coin_balance=F("balance_account__coin_balance"))
.annotate(rank=Window(expression=RowNumber(), order_by="-coin_balance"))
.filter(id=data.get("user_id"))
.first()
)
Есть ли способ избежать гонки или заморозить отфильтрованный набор запросов?
В вашем коде проблема возникает потому, что функции Django Window
, как и RowNumber()
, вычисляют ранг в контексте всего набора запросов. Когда вы применяете filter()
, она уменьшает набор запросов до одного элемента, делая ранг «1». Чтобы сохранить исходный ранг, необходимо вычислить его до фильтрации, а затем отфильтровать результат без изменения аннотированного ранга.
Вот несколько оптимизированных подходов к решению этой проблемы:
1. Использование подзапроса для ранжирования с предварительной фильтрацией
Один из подходов заключается в том, чтобы вычислять ранжирование в подзапросе, а затем фильтровать основной набор запросов. Таким образом, ранжирование сохраняется даже после фильтрации. Вот как это можно реализовать:
from django.db.models import OuterRef, Subquery, Window, F
from django.db.models.functions import RowNumber
# Calculate the rank for each user and store it in a subquery
subquery = MainUser.objects.select_related("balance_account").annotate(
coin_balance=F("balance_account__coin_balance"),
rank=Window(expression=RowNumber(), order_by=F("coin_balance").desc())
).filter(id=OuterRef("pk"))
# Use the subquery to get the user with preserved rank
request_user = MainUser.objects.annotate(
coin_balance=F("balance_account__coin_balance"),
rank=Subquery(subquery.values("rank")[:1])
).filter(id=data.get("user_id")).first()
Этот подход обеспечивает вычисление ранговой аннотации по всему набору пользователей на основе coin_balance
перед фильтрацией.
2. Ранжирование в отдельном запросе и кэше
Другой подход заключается в том, чтобы ранжировать всех пользователей и кэшировать результат, а затем отфильтровать его для конкретного пользователя. Это позволяет не пересчитывать ранг каждый раз и обеспечивает постоянство ранга.
# Cache all users with rank and balance
ranked_users = list(
MainUser.objects.select_related("balance_account")
.annotate(coin_balance=F("balance_account__coin_balance"))
.annotate(rank=Window(expression=RowNumber(), order_by=F("coin_balance").desc()))
)
# Filter the cached list for the user with the specified ID
request_user = next((user for user in ranked_users if user.id == data.get("user_id")), None)
Этот подход оптимален, если после ранжирования всех пользователей вам нужно искать их лишь время от времени. Однако он может оказаться не идеальным для больших наборов данных из-за расхода памяти на хранение всех пользователей в списке.
3. Используйте annotate(rank=Subquery(...))
с материализованным списком
Если ваш набор данных может быть управляемым по памяти, вы можете сначала получить материализованный список пользователей с рангами, а затем отфильтровать его:
# Get all users with ranking in a materialized queryset
all_users_ranked = list(
MainUser.objects.select_related("balance_account")
.annotate(coin_balance=F("balance_account__coin_balance"))
.annotate(rank=Window(expression=RowNumber(), order_by=F("coin_balance").desc()))
)
# Filter to find the specific user by ID without affecting rank
request_user = next((user for user in all_users_ranked if user.id == data.get("user_id")), None)
Каждое из этих решений позволяет избежать повторного ранжирования или потери информации о рангах и повышает эффективность в зависимости от размера ваших данных и ограниченности памяти. Дайте мне знать, если вы хотите дальнейшей оптимизации в каком-либо конкретном направлении!