Django: optimizing a query with spread data

青春壹個敷衍的年華 提交于 2020-01-25 02:24:15

问题


I have Order objects and OrderOperation objects that represent an action on a Order (creation, modification, cancellation).

Conceptually, an order has 1 to many order operations. Each time there is an operation on the order, the total is computed in this operation. Which means when I need to find the total of an order, I just get the last order operation total.

The simplified code

class OrderOperation(models.Model):
    order = models.ForeignKey(Order)
    total = DecimalField(max_digits=9, decimal_places=2)

class Order(models.Model):

    @property
    def last_operation(self) -> Optional['OrderOperation']:
        try:
            qs = self.orderoperation_set.all()
            return qs[len(qs) - 1]
        except AssertionError:  # when there is a negative indexing (no operation)
            # IndexError can not happen
            return None

    @property
    def total(self) -> Optional[Decimal]:
        last_operation = self.last_operation
        return last_operation.total if last_operation else None

The issue

Since I get lots of orders, each time I want to make a simple filtering like "orders that have a total lower than 5€", it takes a long time, because I need to browse all orders, using the following, obviously bad query:

all_objects = Order.objects.all()
Order.objects.prefetch_related('orderoperation_set').filter(
    pk__in=[o.pk for o in all_objects if o.total <= some_value])

My current ideas / what I tried

Data denormalization?

I could simply create a total attribute on Order, and copy the operation total to the order total every time on operation is created. Then, Order.objects.filter(total__lte=some_value) would work. However, before duplicating data in my database, I'd like to be sure there is not an easier/cleaner solution.

Using annotate() method?

I somehow expected to be able to do: Order.objects.annotate(total=something_magical_here).filter(total__lte=some_value). It seems it's not possible.

Filtering separetely then matching?

order_operations = OrderOperation.objects.filter(total__lte=some_value)
orders = Order.objects.filter(orderoperation__in=order_operations)

This is very fast, but the filtering is bad since I didn't filter last operations, but all operations here. This is wrong.

Any other idea? Thanks.


回答1:


Using annotate() method

It seems it's not possible.

Of course, it is possible ;) You can use subqueries or some clever conditional expressions. Assuming that you want to get total amount from last order operation, here is example with subquery:

from django.db.models import Subquery, OuterRef

orders = Order.objects.annotate(
    total=Subquery(                             # [1]
        OrderOperation.objects \
            .filter(order_id=OuterRef("pk")) \  # [2]
            .order_by('-id') \                  # [3]
            .values('total') \                  # [4]
            [:1]                                # [5]
    )
)

Explanation of code above:

  1. We are adding new field to results list, called total taht will be filled in by subquery. You can access it as any other field of model Order in this queryset (either after evaluating it, in model instances or in filtering and other annotations). You can learn how annotation works from Django docs.
  2. Subquery should only be invoked for operations from current order. OuterRef just will be replaced with reference to selected field in resulting SQL query.
  3. We want to order by operation id descending, because we do want latest one. If you have other field in your operations that you want to order by instead (like creation date), fill it here.
  4. That subquery should only return total value from operation
  5. We want only one element. It is being fetched using slice notation instead of normal index, because using index on django querysets will immediately invoke it. Slicing only adds LIMIT clause to SQL query, without invoking it and that is what we want.

Now you can use:

orders.filter(total__lte=some_value)

to fetch only orders that you want. You can also use that annotation to



来源:https://stackoverflow.com/questions/54825556/django-optimizing-a-query-with-spread-data

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!