Order by query results in specific row positions - oracle

This is an extension to a previous quesiton, Order by depending on 2 col values. I have a query which returns the output as below,
SELECT DISTINCT a.rev_date ,fruitname,
fruit_id , primary_fruit_id
FROM fruits a, fruit_lookup s,fruit_reference r
WHERE a.id = s.id(+)
and primary_fruit_id = r.fruit_id(+)
AND (fruit_id = 24 or fruit_id = 0)
ORDER BY case when fruit_id = primary_fruit_id then 0 else 1 end,
fruit_id desc,
a.rev_date desc
How to handle this such that I get the desired output in the below fashion. So, where ever the fruit_id is 0, those rows need to go in the 2nd and the 5th row in the result set of say 10 rows. If totals results are around 50, each page will have 10 results, and in 2nd and the 5th position I need to get the output as below. Hope this makes sense. Any ideas are appreciated. thx
NAME FRUIT_ID PRIMARY_FRUIT_ID
--------------------------------------
apple 24 24
apple 24 24
apple 24 24
apple 24 24
orange 24 12
pear 24 7
kiwi 24 6
melon 24 2
grape 0 90
banana 0 45
carrot 0 30
Desired output
NAME FRUIT_ID PRIMARY_FRUIT_ID
--------------------------------------
apple 24 24
grape 0 90
apple 24 24
apple 24 24
banana 0 45
apple 24 24
kiwi 24 6
orange 24 12
melon 24 2
pear 24 7
carrot 0 30

This is much more complicated question than your previous, so answer is somewhat complicated too.
Here is something what I managed to do:
with t as (
select x.*,
row_number() over (
partition by fid order by decode(fid , pfid, 1, 2), rd desc) rbr
from (
select distinct a.rev_date rd, s.fruitname fn,
a.fruit_id fid, primary_fruit_id pfid
from fruits a left join fruit_lookup s on a.id = s.id
left join fruit_reference r
on primary_fruit_id = r.fruit_id and r.fruit_id in (0, 24) ) x),
ca as (select count(1) cnt from t),
cx as (
select row_number() over (partition by cwm order by lvl) rn, cwm, lvl
from (
select level lvl, case when mod(level, 10) in (2, 5) then 0 else 24 end cwm
from ca connect by level <= cnt*5))
select rd, fn, fid, pfid
from t join cx on cx.rn = t.rbr and cx.cwm = t.fid
order by lvl
You did not provide data structures and sample rows, so I tried to reproduce them to get original input, here is SQLFiddle with data and query.
Subquery cx generates numbers, according to your rules, then these numbers are assigned to your original query
and final select sorts data using these numbers.
The general logic is: insert rows with fruit_id=0 numbered as 2, 5, 12, 15, 22... between other rows.

Related

Is it possible to add distinct to part of a sum clause in Oracle?

I have a pretty lengthy SQL query which I'm going to run on Oracle via hibernate. It consists of two nested selects. In the first select statement, a number of sums are calculated, but in one of them I want to filter the results using unique ids.
SELECT ...
SUM(NVL(CASE WHEN SECOND_STATUS= 50 OR SECOND_STATUS IS NULL THEN RECEIVE_AMOUNT END, 0) +
NVL(CASE WHEN FIRST_STATUS = 1010 THEN AMOUNT END, 0) +
NVL(CASE WHEN FIRST_STATUS = 1030 THEN AMOUNT END, 0) -
NVL(CASE WHEN FIRST_STATUS = 1010 AND (SECOND_STATUS= 50 OR SECOND_STATUS IS NULL) THEN RECEIVE_AMOUNT END, 0)) TOTAL, ...
And at the end:
... FROM (SELECT s.*, p.* FROM FIRST_TABLE s
JOIN SECOND_TABLE p ON s.ID = p.FIRST_ID
In one of the lines that start with NVL (second line actually), I want to add a distinct clause that sums the amounts only if first table ids are unique. But I don't know if this is possible or not. If yes, how would it be?
Assume such setup
select * from first;
ID AMOUNT
---------- ----------
1 10
2 20
select * from second;
SECOND_ID FIRST_ID AMOUNT2
---------- ---------- ----------
1 1 100
2 1 100
3 2 100
After the join you get the total sum of both amounts too high because the amount from the first table is duplicated.
select *
from first
join second on first.id = second.first_id;
ID AMOUNT SECOND_ID FIRST_ID AMOUNT2
---------- ---------- ---------- ---------- ----------
1 10 1 1 100
1 10 2 1 100
2 20 3 2 100
You must add a row_number that identifies the first occurence in the parent table and consider in the AMOUNT only the first row and resets it to NULL in the duplicated rows.
select ID,
case when row_number() over (partition by id order by second_id) = 1 then AMOUNT end as AMOUNT,
SECOND_ID, FIRST_ID, AMOUNT2
from first
join second on first.id = second.first_id;
ID AMOUNT SECOND_ID FIRST_ID AMOUNT2
---------- ---------- ---------- ---------- ----------
1 10 1 1 100
1 2 1 100
2 20 3 2 100
Now you can safely sum in a separate subquery
with tab as (
select ID,
case when row_number() over (partition by id order by second_id) = 1 then AMOUNT end as AMOUNT,
SECOND_ID, FIRST_ID, AMOUNT2
from first
join second on first.id = second.first_id
)
select id, sum(nvl(amount,0) + nvl(amount2,0))
from tab
group by id
;
ID SUM(NVL(AMOUNT,0)+NVL(AMOUNT2,0))
---------- ---------------------------------
1 210
2 120
Note also that this is an answer to your question. Some will argue that in your situation you should first aggregate and than join. This will also resolve your problem possible more elegantly.

How to do a Range Bucket on a Count returned by a Group By query

I was hoping if you can help me.
I am in a situation where I first need to do a Count Distinct of Funds and Group By Policies. Once I have done that, I need to put the count of policies in Range of Number of Funds.
This is the data available, where you can see the policies and different funds linked to it
**PolicyNum Fund**
1201 AB
1202 AC
1203 AB
1203 AC
1203 AD
1204 AB
1204 BC
1204 AC
1204 AD
1204 AE
1204 AF
Now I need to do a Count Distinct of Fund Grouped by Policy.
I have used this query to do that:
select fv, policy, count(distinct fv.fund)
from policy_fund fv
group by fv.policy
order by count(distinct fv.fund) desc
After using the above code, the following would come up
This is a view where you can see the number of funds linked to each policy
**Policy No. of Funds**
1201 1
1202 1
1203 3
1204 6
Now, the problem part, I want to reach to this, which is the Range of Number of Funds and how many policies fall under that range of funds:
Help required to achieve this view of Range of Number of Funds and how many policies are present in each range
**Range of Number of funds Number of policies**
0 to 1 2
2 to 3 1
4 to 5 0
5 to 6 1
You can left join your query to a derived table defining the ranges on the count being in the range and then group by the range and count the policies.
SELECT r.l || ' to ' || r.u "RANGE",
count(p) "COUNT"
FROM (SELECT 0 l,
1 u
FROM dual
UNION ALL
SELECT 2 l,
3 u
FROM dual
UNION ALL
SELECT 4 l,
5 u
FROM dual
UNION ALL
SELECT 6 l,
7 u
FROM dual) r
LEFT JOIN (SELECT fv.policy p,
count(distinct fv.fund) cof
FROM policy_fund fv
GROUP BY fv.policy) fpp
ON fpp.cof >= r.l
AND fpp.cof <= r.u
GROUP BY r.l,
r.u
ORDER BY r.l,
r.u;
db<>fiddle

Sum values by date range in multiple columns

I need to sum value by date range in multiple columns. Every date range is one week of a month. It can be shorter than 7 days if it is the start of the month or the end of the month.
For example, I have dates for February:
my_user my_date my_value
A 01.02.2019 100
A 02.02.2019 150
B 01.02.2019 90
Z 28.02.2019 120
How can I have in date range format such as below?
my_user 01/02-03/02 04/02-10/02 11/02-17/02 18/02-24/02 25/02-28/02
A 250 0 0 0 0
B 90 0 0 0 0
Z 0 0 0 0 120
Any suggestions? Thanks!
You can do this:
select *
from (
select to_char(dt, 'iw') - to_char(trunc(dt, 'month'), 'iw') + 1 wk, usr, val from t)
pivot (sum(val) for wk in (1, 2, 3, 4, 5, 6))
demo
USR 1 2 3 4 5 6
--- ---------- ---------- ---------- ---------- ---------- ----------
A 250
B 90
Z 120
Header numbers are the weeks of month. Maximum may be 6 if month starts at the end of the week and is longer than 28 days.
Similiar way you can find first and last day of each week if needed, but you can't put them as headers, or at least not easily.
Edit:
it is possible to define certain date range with pivot, simple as two
dates? For example, I need to sum values from 5 December 2018 to 4
January 2019, 5 January 2019 to 4 February 2019, 5 March 2019 to 4
April 2019
Yes. Everything depends on how we count first and next weeks. Here:
to_char(dt, 'iw') - to_char(trunc(dt, 'month'), 'iw') + 1
i am subtracting week in year for given date and week in year of first day in month for this date. You can simply replace this second value with your starting date, either by hardcoding it in your query or by sending parameter to query or finding minimum date at first in a subquery:
(to_char(dt, 'iw') - to_char(date '2019-03-05', 'iw')) + 1
or
(to_char(dt, 'iw') - to_char((select min(dt) from data), 'iw')) + 1
Edit 2:
There is one problem however. When user defined period contains two or more years. to_date(..., 'iw') works fine for one year, but for two we get values 51, 52, 01, 02... We have to deal with this somehow, for instance like here:
with t(dt1, dt2) as (select date '2018-12-16', date '2019-01-15' from dual)
select min(dt) mnd, max(dt) mxd, iw, row_number() over (order by min(dt)) rn
from (select dt1 + level - 1 dt, to_char(dt1 + level - 1, 'iw') iw
from t connect by level -1 <= dt2 - dt1)
group by iw
which gives us:
MND MXD IW RN
----------- ----------- -- ----------
2018-12-16 2018-12-16 50 1
2018-12-17 2018-12-23 51 2
2018-12-24 2018-12-30 52 3
2018-12-31 2019-01-06 01 4
2019-01-07 2019-01-13 02 5
2019-01-14 2019-01-15 03 6
In first line we have user defined date ranges. Then I did hierarchical query looping through all dates in range assigning week, then grouped by this week, found start and end dates for each week and assigned row number rn which can be further used by pivot.
You can now simply join your input data with this query, let's name it weeks:
from data join weeks on dt between mnd and mxd
and make pivot. But for longer periods you have to find how many weeks there can be and specify them in pivot clause in (1, 2, 3, 4...). You can also add aliases if you need:
pivot ... for rn in (1 week01, 2 week02... 12 week12)
There is no simply way to avoid listing them manually. If you need it please look for oracle dynamic pivot in SO, there where hundreds similiar questions already. ;-)

Preventing running total from going negative in Oracle

Column 'amount' has value 5 in first row, and value -10 in second row.
Is there a way to make oracle's sum(amount) over() function to return 0 instead of -5 for the second row?
Blatantly using Rajesh Chamarthi's example source: but altering to show more negative and positive... and showing how a case would change all the negative to zero while maintaining the other amounts...
with t as (
select 5 as x, 1 as id from dual
union all
select -10, 2 as id from dual
union all
select 7, 3 as id from dual
union all
select -5, 4 as id from dual
union all
select -2, 5 as id from dual
),
B as (select t.x,
case when sum(x) over (order by id) < 0 then 0
else sum(x) over (order by id)
end Amount
from t
order by id)
Select X, Case when amount < 0 then 0 else amount end as Amount from B;
T Amount
5 5
-10 0
7 2
-5 0
-2 0
----Attempt 2 (1st attempt preserved as comments below reference it)
I couldn't figure out how to interrupt the window function to reset the value to 0 when amount fell below 0... so I used a recursive CTE which gave me greater control.
If id's are not sequential, we could add a row_Number so we have an ID to join on... or we could use min() where > oldID. I assumed we have a single key unique ID or some way of "Sorting" the records in the order you want the sum to occur...
with aRaw as (
select 5 as x, 15 as id from dual
union all
select -10, 20 as id from dual
union all
select 7, 32 as id from dual
union all
select 2, 46 as id from dual
union all
select -15, 55 as id from dual
union all
select 3, 68 as id from dual
),
t as (Select A.*, Row_number() over (order by ID) rn from aRAW A),
CTE(RN, ID, x, SumX) AS (
Select T.RN, T.ID, x, X from t WHERE ID = (Select min(ID) from t)
UNION ALL
Select T.RN, T.ID, T.X, case when T.X+CTE.SumX < 0 then 0 else T.X+Cte.sumX end from T
INNER JOIN CTE
on CTE.RN+1=T.RN)
Select * from cte;
.
CTE: ARaw is just a sample data set
CTE: T adds a sequental row number incase there are gaps in the IDs allowing for a more simple joining approach on the recursive CTE.
CTE: CTE is the recursive CTE that keeps a running total and has a case statement to reset the running total to 0 when it falls below 0
You could use a case statement, but that would not be a true running total
with t as (
select 5 as x, 1 as id from dual
union all
select -10, 2 as id from dual
union all
select 20, 3 as id from dual
union all
select 30, 4 as id from dual
union all
select 10, 5 as id from dual
)
select t.x,
case when sum(x) over (order by id) < 0 then 0
else sum(x) over (order by id)
end running_total
from t
order by id;
X RUNNING_TOTAL
5 5
-10 0
20 15
30 45
10 55

How to sort individual rows in descending order?

I have a table which looks like the following:
Name LastName tPoints aPoints sPoints gPoints type
John Johnny 15 14 13 10 1
Joe P. 12 11 26 10 1
Matt Q. 11 26 37 44 2
Sorine P. 55 9 8 7 2
Ali Ahmed 30 44 88 65 2
... ... .. .. .. .. 3
3
I would like to sort INDIVIDUAL ROWS and display based on TYPE
NOTE: i can't use order by in oracle because it sorts only 1 row and the others
is sorted based on the first row
I don't want to break the table apart into individual tables, then sort it, and then update it back to the original table.
so, the output will looks something like this, for tPoints - i need to display all
15 - John Johnny
12 - Joe P.
and for aPoints
44 - Ali Ahmed
26 - Matt Q.
9 - Sorine P.
and so on ...
in a nutshell, if type = 1 then sort tPoints in descending, if type = 2 then sort aPoints, if type = 3 then sort sPoints, and so on....
what would be an efficient way to chive this?
Regards,
For the sake of simplicity this example includes only two types. Add as many types as you need.
SQL> with t1(Name1, LastName, tPoints, aPoints, sPoints, gPoints, type1) as(
2 select 'John' , 'Johnny', 15, 14, 13, 10, 1 from dual union all
3 select 'Joe' , 'P.' , 12, 11, 26, 10, 1 from dual union all
4 select 'Matt' , 'Q.' , 11, 26, 37, 44, 2 from dual union all
5 select 'Sorine', 'P.' , 55, 9 , 8 , 7, 2 from dual union all
6 select 'Ali' , 'Ahmed' , 30, 44, 88, 65, 2 from dual
7 )
8 select type1
9 , tpoints
10 , apoints
11 , name1
12 , Lastname
13 from t1
14 order by case when type1=1 then tpoints else type1 end desc,
15 case when type1=2 then apoints else type1 end desc;
TYPE1 TPOINTS APOINTS NAME1 LASTNAME
---------- ---------- ---------- ------ --------
1 15 14 John Johnny
1 12 11 Joe P.
2 30 44 Ali Ahmed
2 11 26 Matt Q.
2 55 9 Sorine P.

Resources