Add Alias or annotations without group-by support?

623 views
Skip to first unread message

Cristiano Coelho

unread,
Dec 26, 2017, 1:37:16 PM12/26/17
to Django developers (Contributions to Django itself)
Hello, I'm having a hard time explaining the exact issue but I hope it's clear enough.


Following this issue (https://groups.google.com/forum/#!searchin/django-users/cristiano%7Csort:date/django-users/q6XdfyK29HA/TcE8oFitBQAJ) from django users and a related ticket (https://code.djangoproject.com/ticket/27719) that seems to be left out or forgotten already.

There has to be a way to alias or annotate a value given an expression or SQL Function that doesn't necessarily aggregates data but rather work on a single value.

Right now as shown on the django-users post, using annotate for this purpose will cause unexpected grouping and sub querying that could result in very slow and hard to debug queries.

The core issue is that using annotate without a previous call either vaues or values_list, will work as expected, simply annotating a value and returning it as an additional column, but if an aggregate is added afterwards (such as count), the final query ends up being a redundant query where the annotated value is added to a group by clause (group by id + column), to a column as part of the select (function called twice) and then wrapped into a select * (subquery), which makes the extra column as part of the select and group by useless, unless the query had any kind of left/inner join in which case the group by might make sense (although not sure about the column showing up on the select clause)

The ugly work around is to simply add a .values('id') at the end so the annotated value doesn't show on the group by and select sections, although the nested query still happens.


For this reason, there's currently no way to achieve the above without ugly work arounds or unnecessary database performance hits.

The easiest option I believe would be to follow the ticket in order to implement an alias call that works exactly like annotate but doesn't trigger any grouping.

A more complicated option is probably trying to make annotate/aggregate smarter, so all the unnecessary grouping and sub querying doesn't happen unless needed, for example, if the queryset didn't call values/values_list or if there are no relationships/joins used.


Example/demostration:

Given the following queryset

query1 = MyModel.objects.annotate(x=MyFunction('a', 'b')).filter(x__gte=0.6).order_by('-x')


query1 SQL is good and looks like:

SELECT id, a, b, myfunction(a, b) as x
FROM mymodel
WHERE myfunction
(a, b) >= 0.6
ORDER BY x desc

Notice how there's no group by, the ORM was smart enough to not include it since there was no previous call to values/values_list


If we run query1.count() the final SQL looks like:

SELECT COUNT(*) FROM (
    SELECT id
, myfunction(a, b) as x
    FROM mymodel
    WHERE myfunction
(a ,b) >= 0.6
    GROUP BY id
, myfunction(a ,b)
) subquery

which if myfunction is slow, will add a massive slow down that's not even needed, and should actually be just:

SELECT count(*)
FROM mymodel
WHERE myfunction
(a ,b) >= 0.6


while the other query should ONLY happen if the group by makes sense (i.e, if there's a join somewhere, or a values/values_list was used previously so id is not part of the group by statement)

but if we work around the issue adding a query1.values('id').count(), the final query ends up better:

SELECT COUNT(*) FROM (
    SELECT id
    FROM mymodel
    WHERE myfunction
(a ,b) >= 0.6
) subquery


I hope I could explain this clear enough with the example, and note that using a custom lookup is not possible since the value is required for the order_by to work.


Jared Proffitt

unread,
Mar 7, 2018, 5:48:01 PM3/7/18
to Django developers (Contributions to Django itself)
I have also run into this exact problem. Would love to get this fixed. Have you found a good workaround?

Cristiano Coelho

unread,
Mar 8, 2018, 8:22:00 AM3/8/18
to Django developers (Contributions to Django itself)
The workaround, although extremely ugly and which will probably cause issues in the future (reason I only used it for the model I needed to do those odd queries) was to use a custom queryset/manager. Something like this.

class FasterCountQuerySet(QuerySet):
   
def count(self):
       
return super(FasterCountQuerySet, self.values('pk')).count()
FasterCountManager = Manager.from_queryset(FasterCountQuerySet)

But again, this is extremely ugly and will still cause a subquery, but without the unnecessary group by and extra function calls.

Josh Smeaton

unread,
Mar 9, 2018, 6:27:36 AM3/9/18
to Django developers (Contributions to Django itself)
Would teaching filter() and friends to use expressions directly solve your issue? You suggested using `alias` upthread, but that's only really required so you can refer to it later? Unless you wanted to refer to the field more than once, having each queryset method respect expressions should be enough I think.

https://github.com/django/django/pull/8119 adds boolean expression support to filter. I believe most other queryset methods have support for expressions now (order_by, values/values_list).

For the alias used multiple times case, it should be enough to annotate and then restrict with values if you don't actually want it in the select/group list.

Cristiano Coelho

unread,
Mar 9, 2018, 10:01:41 PM3/9/18
to Django developers (Contributions to Django itself)
It wouldn't work if you also want to order by the annotated value.

Josh Smeaton

unread,
Mar 10, 2018, 6:51:32 AM3/10/18
to Django developers (Contributions to Django itself)
Sure - but you can always save the expression to a variable and use it multiple times.

mycalc = MyFunc('a', 'b')
Model.objects.filter(GreaterEqual(mycalc, 0.6)).order_by(mycalc)

I think we already have the building blocks we need to avoid adding another queryset method.

Cristiano Coelho

unread,
Mar 10, 2018, 11:16:10 AM3/10/18
to Django developers (Contributions to Django itself)
Would that actually end up executing the same function twice?

I didn't state it on the original question, but the biggest issue is that on my use case, the annotation step is actually rather complicated and such wrapped in a method on the model, and then it's up to the external code to filter and sort by the annotated value. Having to use the expression every single time it's needed would defeat the purpose of it.

I agree though that having more methods on the queryset is bad, I would rather improve the annotation logic to be able to handle these cases, but might also be difficult.
Reply all
Reply to author
Forward
0 new messages