Django: агрегат возвращает неверный результат после использования аннотации - PullRequest
0 голосов
/ 04 марта 2019

При агрегировании набора запросов я заметил, что, если я использую аннотацию раньше, я получаю неправильный результат.Я не могу понять, почему.

Код

from django.db.models import QuerySet, Max, F, ExpressionWrapper, DecimalField, Sum
from orders.models import OrderOperation

class OrderOperationQuerySet(QuerySet):
    def last_only(self) -> QuerySet:
        return self \
            .annotate(last_oo_pk=Max('order__orderoperation__pk')) \
            .filter(pk=F('last_oo_pk'))

    @staticmethod
    def _hist_price(orderable_field):
        return ExpressionWrapper(
            F(f'{orderable_field}__hist_unit_price') * F(f'{orderable_field}__quantity'),
            output_field=DecimalField())

    def ordered_articles_data(self):
        return self.aggregate(
            sum_ordered_articles_amounts=Sum(self._hist_price('orderedarticle')))

Тест

qs1 = OrderOperation.objects.filter(order__pk=31655)
qs2 = OrderOperation.objects.filter(order__pk=31655).last_only()
assert qs1.count() == qs2.count() == 1 and qs1[0] == qs2[0]  # shows that both querysets contains the same object

qs1.ordered_articles_data()
> {'sum_ordered_articles_amounts': Decimal('3.72')}  # expected result

qs2.ordered_articles_data()
> {'sum_ordered_articles_amounts': Decimal('3.01')}  # wrong result

Как этоВозможно ли, что этот метод аннотации last_only может сделать результат агрегации другим (и неправильным)?

"Забавно", что кажется, что это происходит только тогда, когда заказ содержит статьи с одинаковым hist_price:enter image description here

Примечание:

  • Я могу подтвердить, что SQL, созданный Django ORM, вероятно, неправильный, потому что, когда я форсируювыполнение last_only() и затем я вызываю агрегацию во втором запросе, он работает как ожидалось.
  • https://docs.djangoproject.com/en/1.11/topics/db/aggregation/#combining-multiple-aggregations может быть объяснением?

SQL-запросы (обратите внимание, что это фактические запросы, но приведенный выше код несколько упрощен, что объясняет наличие ниже COALESCE и "deleted" IS NULL.)

- qs1.ordered_articles_data()

SELECT
    COALESCE(
        SUM(
            ("orders_orderedarticle"."hist_unit_price" * "orders_orderedarticle"."quantity")
        ),
        0) AS "sum_ordered_articles_amounts"
FROM "orders_orderoperation"
    LEFT OUTER JOIN "orders_orderedarticle"
        ON ("orders_orderoperation"."id" = "orders_orderedarticle"."order_operation_id")
WHERE ("orders_orderoperation"."order_id" = 31655 AND "orders_orderoperation"."deleted" IS NULL)

- qs2.ordered_articles_data()

SELECT COALESCE(SUM(("__col1" * "__col2")), 0)
FROM (
    SELECT
        "orders_orderoperation"."id" AS Col1,
        MAX(T3."id") AS "last_oo_pk",
        "orders_orderedarticle"."hist_unit_price" AS "__col1",
        "orders_orderedarticle"."quantity" AS "__col2"
    FROM "orders_orderoperation" INNER JOIN "orders_order"
        ON ("orders_orderoperation"."order_id" = "orders_order"."id")
        LEFT OUTER JOIN "orders_orderoperation" T3
            ON ("orders_order"."id" = T3."order_id")
        LEFT OUTER JOIN "orders_orderedarticle"
            ON ("orders_orderoperation"."id" = "orders_orderedarticle"."order_operation_id")
    WHERE ("orders_orderoperation"."order_id" = 31655 AND "orders_orderoperation"."deleted" IS NULL)
    GROUP BY
        "orders_orderoperation"."id",
        "orders_orderedarticle"."hist_unit_price",
        "orders_orderedarticle"."quantity"
    HAVING "orders_orderoperation"."id" = (MAX(T3."id"))
) subquery

Ответы [ 2 ]

0 голосов
/ 05 апреля 2019

Разделение на подзапросов с меньшими объединениями - это решение для предотвращения проблем с большим количеством объединений дочерних объектов, возможно, с ненужным огромным декартовым произведением независимых наборов или сложным управлением предложением GROUP BY врезультат SQL путем вклада большего числа элементов запроса.

решение : подзапрос используется для получения первичных ключей операций последнего заказа.Простой запрос без добавленных объединений или групп используется, чтобы не искажаться возможной агрегацией по дочерним элементам.

    def last_only(self) -> QuerySet:
        max_ids = (self.values('order').order_by()
                   .annotate(last_oo_pk=Max('order__orderoperation__pk'))
                   .values('last_oo_pk')
                   )
        return self.filter(pk__in=max_ids)

test

ret = (OrderOperationQuerySet(OrderOperation).filter(order__in=[some_order])
       .last_only().ordered_articles_data())

выполненный SQL : (упрощается путем удаления префикса имени приложения order_ и двойных очередей ")

SELECT CAST(SUM((orderedarticle.hist_unit_price * orderedarticle.quantity))
       AS NUMERIC) AS sum_ordered_articles_amounts
FROM orderoperation
LEFT OUTER JOIN orderedarticle ON (orderoperation.id = orderedarticle.order_operation_id)
WHERE (
  orderoperation.order_id IN (31655) AND
  orderoperation.id IN (
    SELECT MAX(U2.id) AS last_oo_pk
    FROM orderoperation U0
    INNER JOIN order U1 ON (U0.order_id = U1.id)
    LEFT OUTER JOIN orderoperation U2 ON (U1.id = U2.order_id)
    WHERE U0.order_id IN (31655)
    GROUP BY U0.order_id
  )
)

Исходный неверный SQL можно исправить, добавив orders_orderedarticle".id вGROUP BY, но только если last_only() и ordered_articles_data() используются вместе.Это не очень хорошо читаемый способ.

0 голосов
/ 04 марта 2019

Когда вы используете любой annotation на языке базы данных ( Агрегатные функции ), вы должны выполнять группирование по всем полям, не входящим в функцию, и вы можете увидеть это внутри подзапроса

GROUP BY
    "orders_orderoperation"."id",
    "orders_orderedarticle"."hist_unit_price",
    "orders_orderedarticle"."quantity"
HAVING "orders_orderoperation"."id" = (MAX(T3."id"))

В результате товары с одинаковыми hist_unit_price и quantity фильтруются по макс. id.Таким образом, на основе вашего экрана одно из chocolate или cafe исключается условием наличия.

...