3

I have a PostgreSQL query that yields the following results:

SELECT   o.order || '-' || osh.ordinal_number AS order, 
         o.company,
         o.order_total,
         SUM(osh.items) AS order_shipment_total,
         o.order_type
FROM     orders o
         JOIN order_shipments osh ON o.order_id = osh.order_id
WHERE    o.order = [some order number]
GROUP BY o.order,
         o.company,
         o.order_total,
         o.order_type;

order   | company | order_total | order_shipment_total | order_type
-------------------------------------------------------------------
123-1   | A corp. | null        |  125.00              | new
123-2   | B corp. | null        |  100.00              | new

I need to replace the o.order_total (it doesn't work properly) and sum up the sum of the order_shipment_total column so that, for the example above, each row winds up saying 225.00. I need the results above to look like this below:

order   | company | order_total | order_shipment_total | order_type
-------------------------------------------------------------------
123-1   | A corp. | 225.00      |  125.00              | new
123-2   | B corp. | 225.00      |  100.00              | new

What I've Tried

1.) To replace o.order_total, I've tried SUM(SUM(osh.items)) but get the error message that you cannot nest aggregate functions.

2.) I've tried to put the entire query as a subquery and sum the order_shipment_total column, but when I do, it just repeats the column itself. See below:

SELECT   order,
         company,
         SUM(order_shipment_total) AS order_shipment_total,
         order_shipment_total,
         order_type
FROM     (
    SELECT   o.order || '-' || osh.ordinal_number AS order, 
             o.company,
             o.order_total,
             SUM(osh.items) AS order_shipment_total,
             o.order_type
    FROM     orders o
             JOIN order_shipments osh ON o.order_id = osh.order_id
    WHERE    o.order = [some order number]
    GROUP BY o.order,
             o.company,
             o.order_total,
             o.order_type
) subquery
GROUP BY order,
         company,
         order_shipment_total,
         order_type;

order   | company | order_total | order_shipment_total | order_type
-------------------------------------------------------------------
123-1   | A corp. | 125.00      |  125.00              | new
123-2   | B corp. | 100.00      |  100.00              | new

3.) I've tried to only include the rows I actually want to group by in my subquery/query example above, because I feel like I was able to do this in Oracle SQL. But when I do that, I get an error saying "column [name] must appear in the GROUP BY clause or be used in an aggregate function."

...
GROUP BY order,
         company,
         order_type;

ERROR:  column "[a column name]" must appear in the GROUP BY clause or be used in an aggregate function.

How do I accomplish this? I was certain that a subquery would be the answer but I'm confused as to why this approach will not work.

2 Answers 2

11

The thing you're not quite grasping with your query / approach is that you're actually wanting two different levels of grouping in the same query row results. The subquery approach is half right, but when you do a subquery that groups, inside another query that groups you can only use the data you've already got (from the subquery) and you can only choose to keep it at the level of aggregate detail it already is, or you can choose to lose precision in favor of grouping more. You can't keep the detail AND lose the detail in order to sum up further. A query-of-subquery is hence (in practical terms) relatively senseless because you might as well group to the level you want in one hit:

SELECT groupkey1, sum(sumx) FROM
(SELECT groupkey1, groupkey2, sum(x) as sumx FROM table GROUP BY groupkey1, groupkey2)
GROUP BY groupkey1

Is the same as:

SELECT groupkey1, sum(x) FROM
table
GROUP BY groupkey1

Gordon's answer will probably work out (except for the same bug yours exhibits in that the grouping set is wrong/doesn't cover all the columns) but it probably doesn't help much in terms of your understanding because it's a code-only answer. Here's a breakdown of how you need to approach this problem but with simpler data and foregoing the window functions in favor of what you already know.

Suppose there are apples and melons, of different types, in stock. You want a query that gives a total of each specific kind of fruit, regardless of the date of purchase. You also want a column for the total for each fruit overall type:

Detail:

fruit | type             | purchasedate | count
apple | golden delicious | 2017-01-01   | 3
apple | golden delicious | 2017-01-02   | 4
apple | granny smith     | 2017-01-04   ! 2
melon | honeydew         | 2017-01-01   | 1
melon | cantaloupe       | 2017-01-05   | 4
melon | cantaloupe       | 2017-01-06   | 2

So that's 7 golden delicious, 2 granny smith, 1 honeydew, 6 cantaloupe, and its also 9 apples and 7 melons

You can't do it as one query*, because you want two different levels of grouping. You have to do it as two queries and then (critical understanding point) you have to join the less-precise (apples/melons) results back to the more precise (granny smiths/golden delicious/honydew/cantaloupe):

SELECT * FROM
(
  SELECT fruit, type, sum(count) as fruittypecount
  FROM fruit
  GROUP BY fruit, type
) fruittypesum
INNER JOIN
(
  SELECT fruit, sum(count) as fruitcount
  FROM fruit
  GROUP BY fruit
) fruitsum
ON
  fruittypesum.fruit = fruitsum.fruit

You'll get this:

fruit | type             | fruittypecount | fruit | fruitcount
apple | golden delicious | 7              | apple | 9
apple | granny smith     | 2              | apple | 9
melon | honeydew         | 1              | melon | 7
melon | cantaloupe       | 6              | melon | 7

Hence for your query, different groups, detail and summary:

SELECT
    detail.order || '-' || detail.ordinal_number as order,
    detail.company,
    summary.order_total,
    detail.order_shipment_total,
    detail.order_type
FROM (
    SELECT   o.order,
             osh.ordinal_number, 
             o.company,
             SUM(osh.items) AS order_shipment_total,
             o.order_type
    FROM     orders o
             JOIN order_shipments osh ON o.order_id = osh.order_id
    WHERE    o.order = [some order number]
    GROUP BY o.order,
             o.company,
             o.order_type
) detail
INNER JOIN
(
    SELECT   o.order,
             SUM(osh.items) AS order_total
    FROM     orders o
             JOIN order_shipments osh ON o.order_id = osh.order_id
    --don't need the where clause; we'll join on order number
    GROUP BY o.order,
             o.company,
             o.order_type
) summary
ON
summary.order = detail.order

Gordon's query uses a window function achieve the same effect; the window function runs after the grouping is done, and it establishes another level of grouping (PARTITION BY ordernumber) which is the effective equivalent of my GROUP BY ordernumber in the summary. The window function summary data is inherently connected to the detail data via ordernumber; it is implicit that a query saying:

SELECT
  ordernumber,
  lineitemnumber,
  SUM(amount) linetotal
  sum(SUM(amount)) over(PARTITION BY ordernumber) ordertotal
GROUP BY
  ordernumber,
  lineitemnumber

..will have an ordertotal that is the total of all the linetotal in the order: The GROUP BY prepares the data to the line level detail, and the window function prepares data to just the order level, and repeats the total as many times are necessary to fill in for every line item. I wrote the SUM that belongs to the GROUP BY operation in capitals.. the sum in lowercase belongs to the partition operation. it has to sum(SUM()) and cannot simply say sum(amount) because amount as a column is not allowed on its own - it's not in the group by. Because amount is not allowed on its own and has to be SUMmed for the group by to work, we have to sum(SUM()) for the partition to run (it runs after the group by is done)

It behaves exactly the same as grouping to two different levels and joining together, and indeed I chose that way to explain it because it makes it more clear how it's working in relation to what you already know about groups and joins

Remember: JOINS make datasets grow sideways, UNIONS make them grow downwards. When you have some detail data and you want to grow it sideways with some more data(a summary), JOIN it on. (If you'd wanted totals to go at the bottom of each column, it would be unioned on)


*you can do it as one query (without window functions), but it can get awfully confusing because it requires all sorts of trickery that ultimately isn't worth it because it's too hard to maintain

Sign up to request clarification or add additional context in comments.

1 Comment

My apologies for taking so long. This is in fact the correct answer; I just opted for your inner join subqueries solution (thanks for explaining the window functions approach as well). I edited your answer slightly to include the FROM keyword, and got rid of an extra comma. Other than that, it's great. Thanks!
2

You should be able to use window functions:

SELECT o.order || '-' || osh.ordinal_number AS order, o.company,
       SUM(SUM(osh.items)) OVER (PARTITION BY o.order) as order_total,
       SUM(osh.items) AS order_shipment_total,
       o.order_type
FROM orders o JOIN
     order_shipments osh
     ON o.order_id = osh.order_id
WHERE o.order = [some order number]
GROUP BY o.order, o.company, o.order_type;

Comments

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.