Search code examples
sqloracleaggregate-functionsrow-numberdense-rank

Oracle - Rank and transform on columns


I got a sample data as below in this cte_orders. Each row is a order and contains the user_id, zone_code and zone_name.

I need to rank the top 3 zone_code for each user_id, and I need the output be on columns. Like this

|user_id |  top1_zone  |  top2_zone  |  top3_zone  |
|--------+-------------+-------------+-------------+
|1000    |  5555-ABCD  |  4567-ZMNY  |  7888-IXPO  |
|--------+-------------+-------------+-------------+
|9999    |  3456-JJKL  |  7688-HBGT  |  5555-ABCD  |
|--------+-------------+-------------+-------------+

Here's the SQL with the test data and the query that I'm trying. The problem with this query is the result is given a row for each position of the rank.

(
    SELECT 1 as order_id, '1000' as user_id, '5555' as zone_code, 'ABCD' as zone_name from dual union all
    SELECT 2 as order_id, '1000' as user_id, '4567' as zone_code, 'ZMNY' as zone_name from dual union all
    SELECT 3 as order_id, '1000' as user_id, '5555' as zone_code, 'ABCD' as zone_name from dual union all
    SELECT 4 as order_id, '1000' as user_id, '5555' as zone_code, 'ABCD' as zone_name from dual union all
    SELECT 5 as order_id, '1000' as user_id, '5599' as zone_code, 'HZTR' as zone_name from dual union all
    SELECT 6 as order_id, '1000' as user_id, '4567' as zone_code, 'ZMNY' as zone_name from dual union all
    SELECT 7 as order_id, '1000' as user_id, '4567' as zone_code, 'ZMNY' as zone_name from dual union all
    SELECT 8 as order_id, '1000' as user_id, '5555' as zone_code, 'ABCD' as zone_name from dual union all
    SELECT 9 as order_id, '1000' as user_id, '1999' as zone_code, 'LNKJ' as zone_name from dual union all
    SELECT 10 as order_id, '1000' as user_id, '5677' as zone_code, 'OPLH' as zone_name from dual union all
    SELECT 11 as order_id, '1000' as user_id, '7888' as zone_code, 'IXPO' as zone_name from dual union all
    SELECT 12 as order_id, '9999' as user_id, '5599' as zone_code, 'HZTR' as zone_name from dual union all
    SELECT 13 as order_id, '9999' as user_id, '3456' as zone_code, 'JJKL' as zone_name from dual union all
    SELECT 14 as order_id, '9999' as user_id, '5555' as zone_code, 'ABCD' as zone_name from dual union all
    SELECT 15 as order_id, '9999' as user_id, '3456' as zone_code, 'JJKL' as zone_name from dual union all
    SELECT 16 as order_id, '9999' as user_id, '5555' as zone_code, 'ABCD' as zone_name from dual union all
    SELECT 17 as order_id, '9999' as user_id, '7688' as zone_code, 'HBGT' as zone_name from dual union all
    SELECT 17 as order_id, '9999' as user_id, '7688' as zone_code, 'HBGT' as zone_name from dual union all
    SELECT 18 as order_id, '9999' as user_id, '1566' as zone_code, 'LNOI' as zone_name from dual union all
    SELECT 19 as order_id, '9999' as user_id, '3456' as zone_code, 'JJKL' as zone_name from dual union all
    SELECT 20 as order_id, '9999' as user_id, '7654' as zone_code, 'NNJJ' as zone_name from dual union all
    SELECT 21 as order_id, '9999' as user_id, '4433' as zone_code, 'NHJE' as zone_name from dual union all
    SELECT 22 as order_id, '9999' as user_id, '4111' as zone_code, 'ABHJ' as zone_name from dual

)

select 
    user_id,
    CASE WHEN rank_zones = 1 then zone_concat end as top1_zone,
    CASE WHEN rank_zones = 2 then zone_concat end as top2_zone,
    CASE WHEN rank_zones = 3 then zone_concat end as top3_zone
    from

( 
    select  
        user_id, 
        zone_code||'-'||zone_name as zone_concat,
        row_number() over (partition by user_id order by count(*) desc) rank_zones 
    from cte_orders
    group by
        user_id,
        zone_code||'-'||zone_name

)
where rank_zones <= 3

group by 
    user_id,
    CASE WHEN rank_zones = 1 then zone_concat end,
    CASE WHEN rank_zones = 2 then zone_concat end,
    CASE WHEN rank_zones = 3 then zone_concat end
order by user_id;

The output that i getting

|user_id |  top1_zone  |  top2_zone  |  top3_zone  |
|--------+-------------+-------------+-------------+
|1000    |  5555-ABCD  |    (null)   |    (null)   |
|--------+-------------+-------------+-------------+
|1000    |    (null)   |  4567-ZMNY  |    (null)   |
|--------+-------------+-------------+-------------+
|1000    |    (null)   |    (null)   |  7888-IXPO  |
|--------+-------------+-------------+-------------+
|9999    |  3456-JJKL  |    (null)   |    (null)   |
|--------+-------------+-------------+-------------+
|9999    |    (null)   |  7688-HBGT  |    (null)   |
|--------+-------------+-------------+-------------+
|9999    |    (null)   |    (null)   |  5555-ABCD  |
|--------+-------------+-------------+-------------+

How can i fix my query to get one row for each user_id without the nulls?


Solution

  • You can use oracle pivot as below:

     with cte_orders as(
         SELECT 1 as order_id, '1000' as user_id, '5555' as zone_code, 'ABCD' as zone_name from dual union all
         SELECT 2 as order_id, '1000' as user_id, '4567' as zone_code, 'ZMNY' as zone_name from dual union all
         SELECT 3 as order_id, '1000' as user_id, '5555' as zone_code, 'ABCD' as zone_name from dual union all
         SELECT 4 as order_id, '1000' as user_id, '5555' as zone_code, 'ABCD' as zone_name from dual union all
         SELECT 5 as order_id, '1000' as user_id, '5599' as zone_code, 'HZTR' as zone_name from dual union all
         SELECT 6 as order_id, '1000' as user_id, '4567' as zone_code, 'ZMNY' as zone_name from dual union all
         SELECT 7 as order_id, '1000' as user_id, '4567' as zone_code, 'ZMNY' as zone_name from dual union all
         SELECT 8 as order_id, '1000' as user_id, '5555' as zone_code, 'ABCD' as zone_name from dual union all
         SELECT 9 as order_id, '1000' as user_id, '1999' as zone_code, 'LNKJ' as zone_name from dual union all
         SELECT 10 as order_id, '1000' as user_id, '5677' as zone_code, 'OPLH' as zone_name from dual union all
         SELECT 11 as order_id, '1000' as user_id, '7888' as zone_code, 'IXPO' as zone_name from dual union all
         SELECT 12 as order_id, '9999' as user_id, '5599' as zone_code, 'HZTR' as zone_name from dual union all
         SELECT 13 as order_id, '9999' as user_id, '3456' as zone_code, 'JJKL' as zone_name from dual union all
         SELECT 14 as order_id, '9999' as user_id, '5555' as zone_code, 'ABCD' as zone_name from dual union all
         SELECT 15 as order_id, '9999' as user_id, '3456' as zone_code, 'JJKL' as zone_name from dual union all
         SELECT 16 as order_id, '9999' as user_id, '5555' as zone_code, 'ABCD' as zone_name from dual union all
         SELECT 17 as order_id, '9999' as user_id, '7688' as zone_code, 'HBGT' as zone_name from dual union all
         SELECT 17 as order_id, '9999' as user_id, '7688' as zone_code, 'HBGT' as zone_name from dual union all
         SELECT 18 as order_id, '9999' as user_id, '1566' as zone_code, 'LNOI' as zone_name from dual union all
         SELECT 19 as order_id, '9999' as user_id, '3456' as zone_code, 'JJKL' as zone_name from dual union all
         SELECT 20 as order_id, '9999' as user_id, '7654' as zone_code, 'NNJJ' as zone_name from dual union all
         SELECT 21 as order_id, '9999' as user_id, '4433' as zone_code, 'NHJE' as zone_name from dual union all
         SELECT 22 as order_id, '9999' as user_id, '4111' as zone_code, 'ABHJ' as zone_name from dual
     
     ),
     cte as(select  
             user_id, 
             zone_code||'-'||zone_name as zone_concat,
             row_number() over (partition by user_id order by count(*) desc ) rank_zones 
         from cte_orders
         group by
             user_id,
             zone_code||'-'||zone_name)
     
     select user_id,"1" top1_zone, "2" top2_zone, "3" top3_zone
     from cte 
     PIVOT (
       MAX(ZONE_CONCAT) FOR rank_zones IN (1,2,3)
     )
     
    

    Output:

    USER_ID TOP1_ZONE TOP2_ZONE TOP3_ZONE
    1000 5555-ABCD 4567-ZMNY 5599-HZTR
    9999 3456-JJKL 5555-ABCD 7688-HBGT

    db<>fiddle here

    Or you can use decode in aggregation with group by:

     with cte_orders as(
         SELECT 1 as order_id, '1000' as user_id, '5555' as zone_code, 'ABCD' as zone_name from dual union all
         SELECT 2 as order_id, '1000' as user_id, '4567' as zone_code, 'ZMNY' as zone_name from dual union all
         SELECT 3 as order_id, '1000' as user_id, '5555' as zone_code, 'ABCD' as zone_name from dual union all
         SELECT 4 as order_id, '1000' as user_id, '5555' as zone_code, 'ABCD' as zone_name from dual union all
         SELECT 5 as order_id, '1000' as user_id, '5599' as zone_code, 'HZTR' as zone_name from dual union all
         SELECT 6 as order_id, '1000' as user_id, '4567' as zone_code, 'ZMNY' as zone_name from dual union all
         SELECT 7 as order_id, '1000' as user_id, '4567' as zone_code, 'ZMNY' as zone_name from dual union all
         SELECT 8 as order_id, '1000' as user_id, '5555' as zone_code, 'ABCD' as zone_name from dual union all
         SELECT 9 as order_id, '1000' as user_id, '1999' as zone_code, 'LNKJ' as zone_name from dual union all
         SELECT 10 as order_id, '1000' as user_id, '5677' as zone_code, 'OPLH' as zone_name from dual union all
         SELECT 11 as order_id, '1000' as user_id, '7888' as zone_code, 'IXPO' as zone_name from dual union all
         SELECT 12 as order_id, '9999' as user_id, '5599' as zone_code, 'HZTR' as zone_name from dual union all
         SELECT 13 as order_id, '9999' as user_id, '3456' as zone_code, 'JJKL' as zone_name from dual union all
         SELECT 14 as order_id, '9999' as user_id, '5555' as zone_code, 'ABCD' as zone_name from dual union all
         SELECT 15 as order_id, '9999' as user_id, '3456' as zone_code, 'JJKL' as zone_name from dual union all
         SELECT 16 as order_id, '9999' as user_id, '5555' as zone_code, 'ABCD' as zone_name from dual union all
         SELECT 17 as order_id, '9999' as user_id, '7688' as zone_code, 'HBGT' as zone_name from dual union all
         SELECT 17 as order_id, '9999' as user_id, '7688' as zone_code, 'HBGT' as zone_name from dual union all
         SELECT 18 as order_id, '9999' as user_id, '1566' as zone_code, 'LNOI' as zone_name from dual union all
         SELECT 19 as order_id, '9999' as user_id, '3456' as zone_code, 'JJKL' as zone_name from dual union all
         SELECT 20 as order_id, '9999' as user_id, '7654' as zone_code, 'NNJJ' as zone_name from dual union all
         SELECT 21 as order_id, '9999' as user_id, '4433' as zone_code, 'NHJE' as zone_name from dual union all
         SELECT 22 as order_id, '9999' as user_id, '4111' as zone_code, 'ABHJ' as zone_name from dual
     
     ),
     cte as(select  
             user_id, 
             zone_code||'-'||zone_name as zone_concat,
             row_number() over (partition by user_id order by count(*) desc ) rank_zones 
         from cte_orders
         group by
             user_id,
             zone_code||'-'||zone_name)
     
     select user_id,
       max(decode(rank_zones,1,ZONE_CONCAT)) top1_zone,
       max(decode(rank_zones,2,ZONE_CONCAT)) top2_zone,
       max(decode(rank_zones,3,ZONE_CONCAT)) top3_zone
     from cte 
     group by user_id
    

    Output:

    USER_ID TOP1_ZONE TOP2_ZONE TOP3_ZONE
    1000 5555-ABCD 4567-ZMNY 5599-HZTR
    9999 3456-JJKL 5555-ABCD 7688-HBGT

    db<>fiddle here