7

Suppose I have two tables:

Foo:

id
baz

Bar:

id
foo_id
boom

So a Foo has many Bars. I frequently find myself in situations where I need to compute an aggregate across the Bars for a given set of Foos but I also want some properties from Foo. The two most straightforward ways of doing this are ugly:

Method #1: Unnecessary Aggregate Functions

select
  foo.id,
  min(foo.baz) as baz,
  min(bar.boom) as min_boom
from
  foo
join
  bar on foo.id = bar.foo_id
group by
  foo.id;

Method #2: Unnecessary Group By Column

select
  foo.id,
  foo.baz,
  min(bar.boom) as min_boom
from
  foo
join
  bar on foo.id = bar.foo_id
group by
  foo.id,
  foo.baz;

This is not so bad when there's only one additional column from Foo besides "id", but if there are many columns that need to be included then the grouping becomes much less efficient. A query like this gets around both these issues, but seems unwieldy:

select
  foo.id,
  foo.baz,
  x.min_boom
from
  foo
join
  (select
    foo_id, 
    min(boom) as min_boom
  from
    bar
  group by
    foo_id) x on x.foo_id = foo.id;

Is there a better way? Platform is Postgres if that matters.

0

2 Answers 2

12

If id is defined as the primary key, you can omit grouping by all the foo columns you want for the output as long as you are grouping by the id. This special case of grouping is in accordance with the current SQL standard and has also been covered in the PostgreSQL manual, starting from version 9.1:

When GROUP BY is present, or any aggregate functions are present, it is not valid for the SELECT list expressions to refer to ungrouped columns except within aggregate functions or when the ungrouped column is functionally dependent on the grouped columns, since there would otherwise be more than one possible value to return for an ungrouped column. A functional dependency exists if the grouped columns (or a subset thereof) are the primary key of the table containing the ungrouped column.

(Emphasis added.)

So, if foo.id is the PK, this query would be valid:

select
  foo.id,
  foo.baz,
  foo.whatever,
  min(bar.boom) as min_boom
from
  foo
join
  bar on foo.id = bar.foo_id
group by
  foo.id;
1
  • 5
    Yes! The restriction is that it has to be the PRIMARY KEY, it hasn't been (yet) implemented for UNIQUE constraints. Commented May 17, 2016 at 7:29
3

As an alternative to grouping, you can use DISTINCT ON, which can make queries significantly simpler and performs very well (often better than aggregates):

select DISTINCT ON (foo.id)
  foo.id,
  foo.baz,
  bar.boom as min_boom
from
  foo
join
  bar on foo.id = bar.foo_id
ORDER BY
  foo.id,
  bar.boom;

Or

select
  foo.id,
  foo.baz,
  x.min_boom
from
  foo
join
  (select DISTINCT ON (foo_id)
    foo_id, 
    boom as min_boom
  from
    bar
  ORDER BY
    foo_id,
    boom) x on x.foo_id = foo.id;
2
  • This doesn't work for the question asked by the OP. In your example you're not using an aggregate function or group by, which is explicitly required to compute the minimum value any bar.boom where bar.foo_id = foo.id has. Or is there some magic in the way you're ordering?
    – iFreilicht
    Commented May 4, 2023 at 15:43
  • 1
    @iFreilicht there is no magic, this is how DISTINCT ON works (in conjunction with ORDER BY). I reworded the answer so it is not confused with an answer (it is a proposed alternative with the same output). The minimum value of bar.boom is obtained in this example as a result of the ORDER BY. Commented Jun 9, 2023 at 2:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.