SQL-запрос Django содержит повторяющийся подзапрос при использовании фильтров с аннотациями и подзапросами
У меня есть модель Camp, которая хранит даты в виде массива строк в поле Json. Я пытаюсь оптимизировать запрос к базе данных для получения объектов Camp с будущими датами.
class Camp(models.Model):
dates = models.JSONField(default=None, blank=True, null=True, validators= [datesListValidator])
Я использую аннотации и подзапросы, чтобы отфильтровать экземпляры этой модели, в которых ни одна из дат не равна или не превышает значения текущая дата (в принципе, мне нужны только лагеря, у которых есть дата в будущем). Я использую аннотации и подзапросы, чтобы получить "max_date" из дат для каждого лагеря, а затем отфильтровать лагеря на основе "max_date"
Я пытаюсь оптимизировать этот процесс фильтрации, так как получение всех лагерей и последующая фильтрация их в Python слишком медленны для моего варианта использования, и с течением времени количество лагерей, которые необходимо отфильтровать, будет только увеличиваться.
Решение, к которому я пришел, дает мне нужные результаты, но сгенерированный SQL-запрос содержит несколько запусков одного и того же подзапроса, который мне не нужен. Я бы хотел избежать использования необработанного SQL и хотел бы найти способ достичь желаемого результата с помощью функций django ORM. Я использую PostgreSQL и готов использовать любые встроенные функции, которые он предоставляет.
Я создал эту функцию, чтобы получить нужные мне лагеря
def get_future_camps():
"""
Retrieves all Camp objects where the latest date in the 'dates' JSONB field is today or in the future.
Returns a QuerySet with only relevant fields.
"""
today_str = now().date().isoformat()
camps_with_max = Camp.objects.annotate(
max_date=Subquery(
Camp.objects.filter(
id=OuterRef("id")
).annotate(
extracted_dates=JsonbArrayElementsText(F("dates"))
).values_list("extracted_dates", flat=True).order_by("-extracted_dates")[:1]
)
)
return camps_with_max.filter(
max_date__gte=today_str
).only("id", "pk")
# This only works for Postgres
class JsonbArrayElementsText(Func):
"""
A custom function to extract elements from a JSONB array as text.
"""
function = 'jsonb_array_elements_text'
output_field = CharField()
Это сгенерировало следующий SQL-запрос
SELECT "my_project_camp"."id",
(
SELECT jsonb_array_elements_text(U0."dates") AS "extracted_dates"
FROM "my_project_camp" U0
WHERE U0."id" = "my_project_camp"."id"
ORDER BY 1 DESC
LIMIT 1
) AS "max_date"
FROM "my_project_camp"
WHERE (
SELECT jsonb_array_elements_text(U0."dates") AS "extracted_dates"
FROM "my_project_camp" U0
WHERE U0."id" = "my_project_camp"."id"
ORDER BY 1 DESC
LIMIT 1
) >= '''2025-03-08'''
При этом используется один и тот же подзапрос для получения "max_date" дважды. Просмотрев его, я обнаружил, что предложения WHERE
вычисляются перед SELECT
, поэтому вы не можете использовать псевдонимы в предложении WHERE
Мой вопрос таков: есть ли способ удалить дополнительный вызов подзапроса? Я использую only
, поэтому мне не нужно использовать поле max_date вне самого фильтра, и весь смысл этого был в том, чтобы оптимизировать получение будущих лагерей
У меня есть модель Camp, которая хранит даты в виде массива строк в поле Json.
вернитесь к моделированию. Большие двоичные объекты в формате JSON - это не хорошая идея <<[django-антипаттерны] если вам нужно работать с частями большого двоичного объекта, это нарушение первой нормальной формы (1NF) [wiki] в реляционных базах данных.
Работайте с дополнительной моделью с помощью:
class Camp(models.Model):
# no dates
# …
pass
class CampDate(models.Model):
camp = models.ForeignKey(Camp, on_delete=models.PROTECT)
date = models.DateField()
Это также гарантирует, что date
является действительной датой, и позволит сохранить ее в базе данных более компактно.
и извлеките с помощью:
from django.utils.timezone import now
today_str = now().date()
Camp.objects.filter(campdate__date__gte=today_str).distinct()