There's a common pattern using row_number() to find either the minimum or the maximum. You can combine them with a little trickery:
select
year,
month,
exp_id,
max(case rn1 when 1 then pickup_ward_text end) as min_pickup_ward_text,
max(case rn2 when 1 then pickup_ward_text end) as max_pickup_ward_text
from (
select
year,
month,
exp_id,
pickup_ward_text,
row_number() over (
partition by year, month, exp_id
order By rating_driver + rating_punctuality + rating_vehicle
) rn1,
row_number() over (
partition by year, month, exp_id
order By rating_driver + rating_punctuality + rating_vehicle desc
) rn2
from
mytable
) x
where
rn1 = 1 or rn2 = 1 -- this line isn't necessary, but might make things quicker
group by
year,
month,
exp_id
order by
year,
month,
exp_id
It may actually be faster to do two derived tables, for each part and inner join them. Some testing is in order:
select
n.year,
n.month,
n.exp_id,
n.pickup_ward_text as min_pickup_ward_text,
x.pickup_ward_text as max_pickup_ward_text
from (
select
year,
month,
exp_id,
pickup_ward_text,
row_number() over (
partition by year, month, exp_id
order By rating_driver + rating_punctuality + rating_vehicle
) rn
from
mytable
) n
inner join (
select
year,
month,
exp_id,
pickup_ward_text,
row_number() over (
partition by year, month, exp_id
order By rating_driver + rating_punctuality + rating_vehicle desc
) rn
from
mytable
) x
on n.year = x.year and n.month = x.month and n.exp_id = x.exp_id
where
n.rn = 1 and
x.rn = 1
order by
year,
month,
exp_id