#24233: group-by column construction is broken
-------------------------------------+-------------------------------------
     Reporter:  megatrump            |                    Owner:  nobody
         Type:  Bug                  |                   Status:  closed
    Component:  Database layer       |                  Version:  1.8alpha1
  (models, ORM)                      |
     Severity:  Normal               |               Resolution:  wontfix
     Keywords:                       |             Triage Stage:  Accepted
    Has patch:  0                    |      Needs documentation:  0
  Needs tests:  0                    |  Patch needs improvement:  0
Easy pickings:  0                    |                    UI/UX:  0
-------------------------------------+-------------------------------------
Changes (by jarshwah):

 * status:  new => closed
 * needs_better_patch:   => 0
 * needs_tests:   => 0
 * needs_docs:   => 0
 * resolution:   => wontfix
 * stage:  Unreviewed => Accepted


Comment:

 Yes, I believe this change was introduced with this PR
 https://github.com/django/django/pull/3669, but it may have been another.
 Grouping was pretty broken when using complicated expressions, because the
 backing columns were being added to the GROUP BY rather than the entire
 expression. It was lucky you were getting the right results.

 Now, the reason this is failing for you now, is because you're effectively
 writing RawSQL (there's a private expression type for this), without any
 of the benefits of new expressions. You may as well be writing `.extra()`
 calls. To fully leverage the power of expressions, you should use the
 smaller types to build up bigger ones. Expressions know that if they
 reference an aggregate, then they shouldn't be added to the group by
 clause. But you've just stuck a raw SUM expression into the roi
 calculation, and Django can't know that it's now referencing an aggregate.

 I've replicated (mostly..) your query only using full expressions. I
 cheated with a couple (using Func(function='..')) rather than building out
 the types fully, but they'll serve for now. I've broken each part down
 into logical chunks, and have printed the underlying query at each stage
 to make this easier to follow.

 First, annotate with the day of week. Annotate can do everything extra can
 do with regards to adding columns to the select list:

 {{{
 In [30]: qs = Costing.objects.annotate(dow=Func(F('ts'), Value('D'),
 function='TO_CHAR')).values('dow')

 In [31]: print(qs.query)
 SELECT TO_CHAR("scratch_costing"."ts", D) AS "dow" FROM "scratch_costing"
 }}}

 Next, add the aggregates we're interested in:

 {{{
 In [32]: qs = qs.annotate(sum_cost=Sum('cost'), sum_rev=Sum('rev'))

 In [33]: print(qs.query)
 SELECT TO_CHAR("scratch_costing"."ts", D) AS "dow",
 SUM("scratch_costing"."cost") AS "sum_cost", SUM("scratch_costing"."rev")
 AS "sum_rev" FROM "scratch_costing" GROUP BY
 TO_CHAR("scratch_costing"."ts", D)
 }}}

 Now, the ROI calculation. Note, this could be improved by using
 [https://docs.djangoproject.com/en/dev/ref/models/conditional-expressions/
 Case/When expressions]

 {{{
 In [34]: qsroi = qs.annotate(roi=Func( Coalesce(F('sum_rev') /
 Coalesce('sum_cost', Value(0)), Value(0)) , Value(2), function='ROUND',
 output_field=FloatField()))

 In [35]: print(qsroi.query)
 SELECT TO_CHAR("scratch_costing"."ts", D) AS "dow",
 SUM("scratch_costing"."cost") AS "sum_cost", SUM("scratch_costing"."rev")
 AS "sum_rev", ROUND(COALESCE((SUM("scratch_costing"."rev") /
 COALESCE(SUM("scratch_costing"."cost"), 0)), 0), 2) AS "roi" FROM
 "scratch_costing" GROUP BY TO_CHAR("scratch_costing"."ts", D)

 In [36]: roi = qsroi[0]
 In [37]: print(roi)
 {'roi': 0.54, 'sum_cost': Decimal('30.30'), 'sum_rev': Decimal('16.50'),
 'dow': u'4'}

 }}}

 And a quick Case/When example, to show how you could avoid doing division
 by NULL or 0:

 {{{
 In [39]: class Round(Func):
    ....:     function = 'ROUND'
    ....:

 In [40]: qsroi =
 qs.annotate(roi=Round(Case(When(Q(sum_cost__isnull=True)|Q(sum_cost=0),
 then='sum_rev'), default=F('sum_rev')/F('sum_cost')), Value(2),
 output_field=FloatField()))

 In [41]: print(qsroi[0])
 {'roi': 0.54, 'sum_cost': Decimal('30.30'), 'sum_rev': Decimal('16.50'),
 'dow': u'4'}

 In [42]: print(qsroi.query)
 SELECT TO_CHAR("scratch_costing"."ts", D) AS "dow",
 SUM("scratch_costing"."cost") AS "sum_cost", SUM("scratch_costing"."rev")
 AS "sum_rev", ROUND(CASE WHEN (SUM("scratch_costing"."cost") IS NULL OR
 SUM("scratch_costing"."cost") = 0) THEN SUM("scratch_costing"."rev") ELSE
 (SUM("scratch_costing"."rev") / SUM("scratch_costing"."cost")) END, 2) AS
 "roi" FROM "scratch_costing" GROUP BY TO_CHAR("scratch_costing"."ts", D)

 }}}

 I've probably put a little too much detail above, but hopefully it'll help
 you solve your problem in a supported way. I'm going to close this as
 wontfix, because the types aren't being used in the right way. If the
 documentation for the expressions or function types needs to be improved,
 please reopen with some comments on where the clarification is required,
 and I'll work on updating them to make it clearer.

--
Ticket URL: <https://code.djangoproject.com/ticket/24233#comment:1>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/067.92bb02f34629cd2cc14729a53bb1e89e%40djangoproject.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to