Django аннотирует с помощью ExtractMonth, а ExtractYear не извлекает year
У меня есть такая модель:
class KeyAccessLog(models.Model):
key = models.ForeignKey(
Key, related_name="access_logs", on_delete=models.CASCADE
)
path = models.CharField(max_length=255)
method = models.CharField(max_length=10)
ip_address = models.GenericIPAddressField()
created = models.DateTimeField(auto_add_now=True)
class Meta:
verbose_name = "Key Access Log"
verbose_name_plural = "Key Access Logs"
ordering = ("-pk",)
indexes = [
models.Index(
models.Index(
"key",
ExtractMonth("created"),
ExtractYear("created"),
name="key_month_year_idx",
),
),
]
def __str__(self):
return f"{self.key} - {self.path} - {self.method}"
Моя идея заключается в том, чтобы использовать объявленный индекс при фильтрации по key, month, и year, но запрос, сгенерированный из ORM, не извлекается так, как это происходит за месяц.
dt_now = timezone.now()
qs = key.access_logs.filter(created__month=dt_now.month, created__year=dt_now.year)
print(qs.query)
дает мне:
SELECT
"public_keyaccesslog"."id",
"public_keyaccesslog"."key_id",
"public_keyaccesslog"."path",
"public_keyaccesslog"."method",
"public_keyaccesslog"."ip_address",
"public_keyaccesslog"."created"
FROM
"public_keyaccesslog"
WHERE
(
"public_keyaccesslog"."key_id" = 1
AND EXTRACT(
MONTH
FROM
"public_keyaccesslog"."created" AT TIME ZONE 'UTC'
) = 5
AND "public_keyaccesslog"."created" BETWEEN '2025-01-01 00:00:00+00:00' AND '2025-12-31 23:59:59.999999+00:00'
)
ORDER BY
"public_keyaccesslog"."id" DESC
Теперь я попытался явно аннотировать извлеченные месяц и год:
dt_now = timezone.now()
qs = key.access_logs.annotate(
month=ExtractMonth("created"),
year=ExtractYear("created")
).filter(month=dt_now.month, year=dt_now.year)
print(qs.query)
это дает мне почти тот же SQL:
SELECT
"public_keyaccesslog"."id",
"public_keyaccesslog"."key_id",
"public_keyaccesslog"."path",
"public_keyaccesslog"."method",
"public_keyaccesslog"."ip_address",
"public_keyaccesslog"."created",
EXTRACT(
MONTH
FROM
"public_keyaccesslog"."created" AT TIME ZONE 'UTC'
) AS "month",
EXTRACT(
YEAR
FROM
"public_keyaccesslog"."created" AT TIME ZONE 'UTC'
) AS "year"
FROM
"public_keyaccesslog"
WHERE
(
"public_keyaccesslog"."key_id" = 1
AND EXTRACT(
MONTH
FROM
"public_keyaccesslog"."created" AT TIME ZONE 'UTC'
) = 5
AND "public_keyaccesslog"."created" BETWEEN '2025-01-01 00:00:00+00:00' AND '2025-12-31 23:59:59.999999+00:00'
)
ORDER BY
"public_keyaccesslog"."id" DESC
Год никогда не фильтруется по извлеченному значению, как месяц, и тогда мой индекс не имеет никакого смысла.
Я бы ожидал, что это будет:
SELECT
"public_keyaccesslog"."id",
"public_keyaccesslog"."key_id",
"public_keyaccesslog"."path",
"public_keyaccesslog"."method",
"public_keyaccesslog"."ip_address",
"public_keyaccesslog"."created",
EXTRACT(
MONTH
FROM
"public_keyaccesslog"."created" AT TIME ZONE 'UTC'
) AS "month",
EXTRACT(
YEAR
FROM
"public_keyaccesslog"."created" AT TIME ZONE 'UTC'
) AS "year"
FROM
"public_keyaccesslog"
WHERE
(
"public_keyaccesslog"."key_id" = 1
AND EXTRACT(
MONTH
FROM
"public_keyaccesslog"."created" AT TIME ZONE 'UTC'
) = 5
AND EXTRACT(
YEAR
FROM
"public_keyaccesslog"."created" AT TIME ZONE 'UTC'
) = 2025
)
ORDER BY
"public_keyaccesslog"."id" DESC
Есть какое-нибудь решение для этого?
"Виновник", если хотите, - это какая-то пользовательская логика, добавленная Django для этого. Действительно:
class YearExact(YearLookup, Exact): def get_direct_rhs_sql(self, connection, rhs): return "BETWEEN %s AND %s" def get_bound_params(self, start, finish): return (start, finish)
, который регистрируется как __exact поиск объекта ExtractYear (не путать с YearExact таким образом).
Вероятно, также не имеет особого смысла добавлять отдельный индекс для года: простое добавление индекса для created будет иметь (почти) ту же производительность, поскольку он выполняет логарифмический поиск, и использование индекса для ExtractYear это может привести к дополнительному ведению бухгалтерского учета, поскольку, вероятно, к 2025 году появится много записей.
Если вы действительно хотите избавиться от этого, вы можете просто зарегистрировать свой собственный ExtractYear, например:
from django.db.models.functions.datetime import ExtractYear
from django.db.models.lookups import Exact
class MyExtractYear(ExtractYear):
pass
MyExtractYear.register_lookup(Exact)
и, таким образом, вместо этого используйте MyExtractYear. Но это, вероятно, не стоит усилий и может даже привести к снижению эффективности запросов.