Home > Software design >  How do I reverse the sign in a django database query on matching rows?
How do I reverse the sign in a django database query on matching rows?

Time:02-13

I have a Django model that looks like this:

direction_choices = (
    ("up", "Up"),
    ("down", "Down"),
    ("left", "Left"),
    ("right", "Right"),
)

class Movement:
    direction = models.CharField(max_length=5, choices=direction_choices)
    distance = models.PositiveSmallIntegerField()

Lets say the data in my database looks like this:

Movement(direction="up", 4)
Movement(direction="up", 6)
Movement(direction="down", 3)
Movement(direction="down", 1)
Movement(direction="left", 7)
Movement(direction="right", 4)
Movement(direction="left", 1)
Movement(direction="right", 8)

I want to do a database query that will calculate the total distance moved in the Y axis.

In our example, it would be: 4 6-3-1 = 6

I can do something like:

Movement.objects.all().filter(
    direction__in=("up", "down")
    ).aggregate(
        total=Sum('distance')
    )

which gives me:

{'total': 16}

I'm not sure how to tell the database that "down" fields should be multiplied by -1 to have their signs reversed before adding up the total.

I know this is easy to do with Python with a for loop, or list comprehension. How would one have the database do this?

CodePudding user response:

You can aggregate with:

from django.db.models import Q

Movement.objects.filter(
    direction__in=('up', 'down')
).aggregate(
    total=Sum('distance', filter=Q(direction='up'))
        - Sum('distance', filter=Q(direction='down'))
)

or you can work through annotation:

from django.db.models import Case, F, When

Movement.objects.filter(
    direction__in=('up', 'down')
).alias(
    dir=Case(
        When(direction='down', then=-F('distance')),
        default=F('distance')
    )
).aggregate(
    total=Sum('dir')
)

CodePudding user response:

I would do this using a group by to only sum within each direction, followed by doing the final subtraction in Python:

result = Movement.objects.values('direction').order_by('direction').annotate(total_distance=Sum('distance'))
# Transform to dict
result = {row['direction']: row['total_distance'] for row in result}
y_axis = result['up'] - result['down']
x_axis = result['right'] - result['left']

What's the advantage of this approach? It produces a more efficient query, which only needs to iterate over the data once, which is helpful if you have a lot of rows.

  • Related