Search code examples
sqloracleexasol

GROUP BY one column, then GROUP BY another column


I have a database table t with a sales table:

ID TYPE AGE
1 B 20
1 BP 20
1 BP 20
1 P 20
2 B 30
2 BP 30
2 BP 30
3 P 40

If a person buys a bundle it appears the bundle sale (TYPE B) and the different bundle products (TYPE BP), all with the same ID. So a bundle with 2 products appears 3 times (1x TYPE B and 2x TYPE BP) and has the same ID.

A person can also buy any other product in that single sale (TYPE P), which has also the same ID.

I need to calculate the average/min/max age of the customers but the multiple entries per sale tamper with the correct calculation.

The real average age is

(20 + 30 + 40) / 3 = 30

and not

(20+20+20+20 + 30+30+30 + 40) / 8 = 26,25

But I don't know how I can reduce the sales to a single row entry AND get the 4 needed values?

Do I need to GROUP BY twice (first by ID, then by AGE?) and if yes, how can I do it?

My code so far:

SELECT
      AVERAGE(AGE)
    , MIN(AGE)
    , MAX(AGE)
    , MEDIAN(AGE)
FROM t

but that does count every row.


Solution

  • Assuming the age is the same for all rows with the same ID (which in itself indicates a normalisation problem), you can use nest aggregation:

    select avg(min(age)) from sales
    group by id
    
    AVG(MIN(AGE))
    -------------
               30
    

    SQL Fiddle

    The example in the documentation is very similar; and is explained as:

    This calculation evaluates the inner aggregate (MAX(salary)) for each group defined by the GROUP BY clause (department_id), and aggregates the results again.

    So for your version:

    This calculation evaluates the inner aggregate (MIN(age)) for each group defined by the GROUP BY clause (id), and aggregates the results again.

    It doesn't really matter whether the inner aggregate is min or max - again, assuming they are all the same - it's just to get a single value per ID, which can then be averaged.


    You can do the same for the other values in your original query:

    select
      avg(min(age)) as avg_age,
      min(min(age)) as min_age,
      max(min(age)) as max_age,
      median(min(age)) as med_age
    from sales
    group by id;
    
    AVG_AGE MIN_AGE MAX_AGE MED_AGE
    ------- ------- ------- -------
         30      20      40      30
    

    Or if you prefer you could get the one-age-per-ID values once ina CTE or subquery and apply the second layer of aggregation to that:

    select 
      avg(age) as avg_age,
      min(age) as min_age,
      max(age) as max_age,
      median(age) as med_age
    from (
       select min(age) as age
       from sales
       group by id
    );
    

    which gets the same result.

    SQL Fiddle