Django ORM не генерирует правильный SQL для многих, многие не в - PullRequest
1 голос
/ 05 апреля 2019

У меня проблема с сгенерированным Django SQL из ORM.

Cartons имеют отношения многие ко многим с Shipments через cartons_shipments.

Япытаясь исключить отгрузки, у которых есть хотя бы одна INBOUND коробка, которая имеет статус ['TRANSIT', 'DELIVERED', 'FAILURE'].

Но я не получил ожидаемых результатов, поэтому включил ведение журнала SQL.

return Shipment.objects.filter(
    ... # other filtering
    # does not have any inbound cartons in_transit/delivered/failed
    ~Q(
        Q(cartons__type='INBOUND') &
        Q(cartons__status__in=['TRANSIT', 'DELIVERED', 'FAILURE'])
    ) &
).distinct()

Я также попробовал это как мой фильтр, но получил тот же вывод SQL.

~Q(
    cartons__type='INBOUND', 
    cartons__status__in=['TRANSIT', 'DELIVERED', 'FAILURE']
)

Это генерирует этот SQL:

AND NOT (
    "shipments"."id" IN (
        SELECT U1."shipment_id" 
        FROM "cartons_shipments" U1 
        INNER JOIN "cartons" U2 ON (U1."carton_id" = U2."id") 
        WHERE U2."type" = 'INBOUND'
    ) 
    AND "shipments"."id" IN (
        SELECT U1."shipment_id" FROM "cartons_shipments" U1 
        INNER JOIN "cartons" U2 ON (U1."carton_id" = U2."id") 
        WHERE U2."status" IN ('TRANSIT', 'DELIVERED', 'FAILURE')
    )
) 

Но это не правильно, так какэто исключило бы отправления, для которых любая INBOUND картонных коробок и отправления, в которых любая картонная коробка (не обязательно INBOUND картонная коробка) имеет статус в ['TRANSIT', 'DELIVERED', 'FAILURE'].Мне нужно объединить эту логику.

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

Правильный SQL будет чем-тонапример:

AND NOT ("shipments"."id" IN (
    SELECT U1."shipment_id" 
    FROM "cartons_shipments" U1 
    INNER JOIN "cartons" U2 ON (U1."carton_id" = U2."id") 
    WHERE U2."type" = 'INBOUND'
    and U2."status" IN ('TRANSIT', 'DELIVERED', 'FAILURE')
))

Таким образом, я бы исключал только посылки с INBOUND коробками в этих статусах.

Время запроса между этими двумя значениями является значительным, и, конечно, я могуполучить правильные результаты со вторым примером SQL.Я думал, что смогу объединить эту логику, комбинируя объекты Q().Но не могу понять.

Я также подумал, что, возможно, я мог бы просто исправить необработанный SQL во 2-м примере.Но мне трудно разобраться, как объединить raw sql с другими фильтрами ORM.

Любая помощь будет принята с благодарностью.


Редактировать:

Я могу получить правильный результат, выполнив фильтрацию в коде и удалив фильтр из запроса:

returned_cartons = Carton.objects.prefetch_related('shipments').filter(
    type='INBOUND',
    status__in=['TRANSIT', 'DELIVERED', 'FAILURE']
)

returned_shipment_ids = list(map(
    lambda carton: carton.shipments.first().id,
    returned_cartons
))

return list(filter(
    lambda shipment: shipment.id not in returned_shipment_ids,
    shipments
))

К сожалению, это слишком медленно, чтобы быть полезным.


Окончательное решение, основанное на идее Endre Both ?

return Shipment.objects.filter(
    ...,  # other filtering
    # has at least 1 inbound carton
    Q(cartons__type='INBOUND')
).exclude(
    # we want to exclude shipments that have at least 1 inbound cartons
    # with a status in transit/delivered/failure
    id__in=Shipment.objects.filter(
        ...,  # filters to limit the number of records returned
        cartons__type='INBOUND',
        cartons__status__in=['TRANSIT', 'DELIVERED', 'FAILURE'],
    ).distinct()
).distinct()

Эта строка Q(cartons__type='INBOUND') обязательна, поскольку мы исключаем Отправления, имеющие INBOUND Картон в статусах ['TRANSIT', 'DELIVERED', 'FAILURE'].Но мы также сохранили бы отгрузки, в которых нет коробок.

Надеюсь, это поможет большему количеству людей.

1 Ответ

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

Для нас, простых смертных, «М» в ORM может быть немного непостижимым.Но вы можете попробовать другой, более простой способ.Он по-прежнему использует подзапрос, а не объединение, но это не обязательно перетаскивание производительности.

Shipment.objects.exclude(
    id__in=Cartons.objects
        .filter(type='INBOUND',
                status__in=['TRANSIT', 'DELIVERED', 'FAILURE'])
        .values('shipments__id')
        .distinct()
)

Точное имя ссылки на первичный ключ Shipment из модели Carton зависит отточное определение моделей.Я использовал shipments__id, но это может быть shipment_set__id или что-то еще.


Новая идея: вы должны основывать подвыбор на промежуточной модели, а не Cartons.Если у вас есть явная промежуточная модель, это легко, а если нет, сначала вам нужен объект Shipment или Cartons, потому что, насколько я знаю, вы не можете получить ссылку на промежуточную модель из самого класса,только из экземпляра.

IModel = Shipment.objects.first().cartons.through
Shipment.objects.exclude(
    id__in=IModel.objects
        .filter(cartons__type='INBOUND',
                cartons__status__in=['TRANSIT', 'DELIVERED', 'FAILURE'])
        .values('shipment__id')
        .distinct()
)
...