Using RANK() to assign a rank based on multiple criteria in Oracle - oracle

I have a table similar to:
Item
Class
Qty
Loc
Apple
Fruit1
1
N
Apple
Fruit1
1
NW
Apple
Fruit2
0
W
Apple
Fruit3
1
N
Orange
Fruit1
10
SE
Orange
Fruit2
1
SW
I am trying to query all rows with the lowest Class that has the smallest non-zero Qty for each Item. So if the same Item has the same Qty for multiple classes, it would select the lowest Class (Fruit1 < Fruit2 < Fruit3 < ...). I've tried using something like:
SELECT A.*,
RANK() OVER(
PARTITION BY ITEM, CLASS
ORDER BY QTY ASC, CLASS ASC) AS item_rank
FROM fruits.info
WHERE QTY <> 0
to get a ranking to select, which isn't working. The results of the ranking should be:
Item
Class
Qty
Loc
item_rank
Apple
Fruit1
1
N
1
Apple
Fruit1
1
NW
1
Apple
Fruit3
1
N
2
Orange
Fruit1
10
SE
2
Orange
Fruit2
1
SW
1
I've would then use a nested select for item_rank = 1:
SELECT B.* FROM (
SELECT A.*,
RANK() OVER(
PARTITION BY ITEM, CLASS
ORDER BY QTY ASC, CLASS ASC) AS item_rank
FROM fruits.info
) B
WHERE B.item_rank = 1
to get the final results, which should be:
Item
Class
Qty
Loc
item_rank
Apple
Fruit1
1
N
1
Apple
Fruit1
1
NW
1
Orange
Fruit2
1
SW
1
How can I structure my RANK() in a way to achieve this? Is there a more efficient way?

class should only be used in ordering, not in partitioning. You want ranks for each item, not for each pair of item and class.
The (easy to fix) mistake in your attempt was to include class both in partition by and in order by; if you think about it for a second, that NEVER makes sense: if you partition by something, then further ordering by it (in each partition, where "it" is constant) makes no sense. Remove class from partition by and you should get what you need.

Related

Find number of position n each deparment

Tables CRIMES, SUSPECTS
How many women and how many men have committed crimes in the car theft.
table crimes:
1 2 3 so on...
table Suspects:
1 2 3 so on...
I made some work, but my soulition did not accepted by SQL
select name, sex, crime_type, count(case when sex='Male' then 1 end)
as male_cnt, count(case when sex='Female' then 1 end) as female_cnt
from suspects NATURAL JOIN crimes where crime_type='Car Theft' group
by name, sex;
Blockquote
How many women and how many men have committed crimes in the car theft.
The question only asks for the gender and the count so you do not need to include the name or the crime type in the output.
If you want a single row then you want to aggregate over the entire result-set and can use your conditional aggregation for each gender:
select count(case when s.sex='Male' then 1 end) as male_cnt,
count(case when s.sex='Female' then 1 end) as female_cnt
from suspects s
INNER JOIN crimes c
ON (s.suspect_id = c.suspect_id)
where c.crime_type='Car Theft';
or, if you want one row-per-gender then:
select s.sex,
count(*) as cnt
from suspects s
INNER JOIN crimes c
ON (s.suspect_id = c.suspect_id)
where c.crime_type='Car Theft'
GROUP BY s.sex;

How does the recursive WITH query work in oracle? When does it go into a cycle?

I have a scenario where I have to display a row 'n' number of times depending on the value in its quantity column.
Item Qty
abc 2
cde 1
Item Qty
abc 1
abc 1
cde 1
I am looking to convert the first table to the second.
I came across the site that I should be using the recursive WITH query.
My anchor member returns the original table.
SELECT ITEM, QTY
FROM lines
WHERE
JOB = TO_NUMBER ('1')
AND ITEM IN
(SELECT PART
FROM PICK
WHERE DELIVERY = '2')
My recursive member is as follows.
SELECT CTE.ITEM, (CTE.QTY - 1) QTY
FROM CTE
INNER JOIN
(SELECT ITEM, QTY
FROM LINES
WHERE JOB_ID = TO_NUMBER ('1')
AND ITEM IN
(SELECT PART
FROM PICK
WHERE DELIVERY = '2'
)) T
ON CTE.ITEM = T.ITEM
WHERE CTE.QTY > 1
My goal is to get all the parts and quantities first then and then for all parts with qty > 1 in the recursive step generate new rows to be added to the original result set and qty displayed in the new rows would be (original qty for that part - 1). The recursion would go on until qty becomes 1 for all the parts.
So this is what I had in the end.
WITH CTE (ITEM, QTY)
AS (
SELECT ITEM, QTY
FROM lines
WHERE
JOB = TO_NUMBER ('1')
AND ITEM IN
(SELECT PART
FROM PICK
WHERE DELIVERY = '2')
UNION ALL
SELECT CTE.ITEM, (CTE.QTY - 1) QTY
FROM CTE
INNER JOIN
(SELECT ITEM, QTY
FROM LINES
WHERE JOB_ID = TO_NUMBER ('1')
AND ITEM IN
(SELECT PART
FROM PICK
WHERE DELIVERY = '2'
)) T
ON CTE.ITEM = T.ITEM
WHERE CTE.QTY > 1)
SELECT ITEM, QTY
FROM CTE
ORDER BY 1, 2 DESC
I get the following error when I try the above
"ORA-32044: cycle detected while executing recursive WITH query"
How is it getting into a cycle? What did I miss in its working?
Also, Upon reading from another website If I used a "cycle clause". I was able to stop the cycle.
The clause I used was.
CYCLE
QUANTITY
SET
END TO '1'
DEFAULT '0'
If I used this before the select statement. I'm getting the desired output but I don't feel this is the right way of going about it. What exactly is the clause doing? What is the right way of using it?
Oracle Setup:
CREATE TABLE lines ( Item, Qty ) AS
SELECT 'abc', 2 FROM DUAL UNION ALL
SELECT 'cde', 1 FROM DUAL;
CREATE TABLE pick ( part, delivery ) AS
SELECT 'abc', 2 FROM DUAL UNION ALL
SELECT 'cde', 2 FROM DUAL;
Query 1: Using a hierarchical query:
SELECT Item,
COLUMN_VALUE AS qty
FROM lines l
CROSS JOIN
TABLE(
CAST(
MULTISET(
SELECT 1
FROM DUAL
CONNECT BY LEVEL <= l.Qty
)
AS SYS.ODCINUMBERLIST
)
) t
WHERE item IN ( SELECT part FROM pick WHERE delivery = 2 )
Query 2: Using a recursive sub-query factoring clause:
WITH rsqfc ( item, qty ) AS (
SELECT item, qty
FROM lines l
WHERE item IN ( SELECT part FROM pick WHERE delivery = 2 )
UNION ALL
SELECT item, qty - 1
FROM rsqfc
WHERE qty > 1
)
SELECT item, 1 AS qty
FROM rsqfc;
Output:
ITEM | QTY
:--- | --:
abc | 1
abc | 1
cde | 1
db<>fiddle here

Calculating values in date ranges in Oracle (possibly with recursive CTE)

I have a problem which can be handled by a recursive CTE, but not within an acceptable period of time. Can anyone point me at ways to improve the performance and/or get the same result a different way?
Here's my scenario!
I have : A large table which contains in each row an id, a start date, an end date, and a ranking number. There are multiple rows for each id and the date ranges often overlap. Dates are from 2010 onward.
I want: A table which contains a row for each combination of id + date which falls inside any date range for that id from the previous table. Each row should have the lowest ranking number for that id and day.
Eg:
ID Rank Range
1 1 1/1/2010-1/4/2010
1 2 1/2/2010-1/5/2010
2 1 1/1/2010-1/2/2010
becomes
ID Rank Day
1 1 1/1/2010
1 1 1/2/2010
1 1 1/3/2010
1 1 1/4/2010
1 2 1/5/2010
2 1 1/1/2010
2 1 1/2/2010
I can do this with a recursive CTE, but the performance is terrible (20-25 minutes for a relatively small data set which produces a final table with 31 million rows):
with enc(PersonID, EncounterDate, EndDate, Type_Rank) as (
select PersonID, EncounterDate, EndDate, Type_Rank
from Big_Base_Table
union all
select PersonID, EncounterDate + 1, EndDate, Type_Rank
from enc
where EncounterDate + 1 <= EndDate
)
select PersonID, EncounterDate, min(Type_Rank) Type_Rank
from enc
group by PersonID, EncounterDate
;
You could extract all possible dates from the table once in a CTE, and then join that back to the table:
with all_dates (day) as (
select start_date + level - 1
from (
select min(start_date) as start_date, max(end_date) as end_date
from big_base_table
)
connect by level <= end_date - start_date + 1
)
select bbt.id, min(bbt.type_rank) as type_rank, to_char(ad.day, 'YYYY-MM-DD') as day
from all_dates ad
join big_base_table bbt
on bbt.start_date <= ad.day
and bbt.end_date >= ad.day
group by bbt.id, ad.day
order by bbt.id, ad.day;
ID TYPE_RANK DAY
---------- ---------- ----------
1 1 2010-01-01
1 1 2010-01-02
1 1 2010-01-03
1 1 2010-01-04
1 2 2010-01-05
2 1 2010-01-01
2 1 2010-01-02
7 rows selected.
The CTE gets all dates from the lowest for any ID, up to the highest for any ID. You could also use a static calendar table for that if you have one, to save hitting the table twice (and getting min/max at the same time is slow in some versions at least).
You could also write it the other way round, as:
...
from big_base_table bbt
join all_dates ad
on ad.day >= bbt.start_date
and ad.day <= bbt.end_date
...
but I think the optimisier will probably end up treating them the same, with a single full scan of your base table; worth checking the plan it actually comes up with for both though, and if one is more efficnet that the other.

Oracle - Group by the most frequent entries

I have 2 tables on my Oracle DB
One with a product list
PRODUCT_ID - PRODUCT_NAME - PRODUCT_PRICE
1 P_1 50
2 P_2 60
3 P_3 70
4 P_4 80
And one with the orders
CLIENT_ID - PRODUCT_ID - ORDER_PRICE
1 1 50
2 3 60
3 2 70
4 2 70
I need to make a query so it returns the product_list table but ordered by the most frequent Product_id in the orders table. So in this case the Product ID=2 must be first on the list.
I have found some examples but i cant find something that will work for this case.
You can use subquery for aggregation on orders table to find count for each product id and then left join it with the product_list table to use the calculated count for ordering.
select p.*
from product_list p
left join (
select product_id,
count(*) as cnt
from orders
group by product_id
) o on p.product_id = o.product_id
order by o.cnt desc nulls last;
LEFT Join is used since not all products could have orders and we need to find the count of orders for each product.
GROUP BY is used because we use the aggregate count() to find the occurrence of orders for a given Product.
ORDER BY DESC is used so the count is ordered highest count of product orders first to lowest. However when ties exist, we don't know what order will be returned as a second level of order by is not defined. Could be order We could add a Product_ID so they are low to high after that...
.
SELECT PL.Product_ID, PL.Product_Name, PL.Product_Price, count(O.Product_ID) cnt
FROM Product_List
LEFT JOIN Orders O
on O.Product_ID = PL.Product_ID
GROUP BY PL.Product_ID, PL.Product_Name, PL.Product_Price
ORDER BY cnt Desc

removing subselect to optimize this query

I have this query that takes about 5 minutes to return a result set and I can't figure out a better way to do it. The table in question has about 15 or 20 million rows at all times and its schema can be summarized as
create table conversation(
id raw, -- GUID
vendor varchar,
snumber number
rcvdate date
)
where we store messages sent from or to a vendor, each message has a sequence number shared by all the conversation (set of related messages). The problem comes because the vendor can have a parent and the message can have the parent's code (we can assume we know both the code of the vendor and their parent's code at the time of the query). Suppose that A and B are 2 vendors with a common parent P, the table might look like
Vendor snumber date
------------------------------
A 1 01-JAN-2012
P 1 02-JAN-2012
A 1 02-JAN-2012
A 2 03-JAN-2012
P 2 03-JAN-2012
B 3 03-JAN-2012
P 3 04-JAN-2012
A 2 04-JAN-2012
We need to query the last N messages from/to A and get the messages with vendor=A OR (vendor=P and another record with vendor=A and same snumber), that is:
Vendor snumber date
------------------------------
A 1 01-JAN-2012
P 1 02-JAN-2012
A 1 02-JAN-2012
A 2 03-JAN-2012
P 2 03-JAN-2012
A 2 04-JAN-2012
What I did was to store the conversations to/from A in a temporary table T(id, snumber) and then return
select * from (
select * from conversations c
where
exists (select id from T where T.id = C.id) or
( c.vendor=l_parent and exists (select snumber from T where T.snumber=c.snumber )
) where rownum <= l_N
those 2 subqueries are killing the performance. The conversations table has indexes in all the columns I included in this example.
I'm thinking the has to a be a clever way to group this information without having to use temporary tables or subqueries but I can't think of one. Any help will be appreciated.
it sounds like you want something like this:
SQL> select vendor, snumber, rcvdate
2 from (select vendor, snumber, rcvdate,
3 max(case when vendor = 'A' then 'Y' end) over (partition by snumber) has_vendor
4 from conversation
5 where vendor in ( 'A', 'P' )
6 order by rcvdate desc)
7 where has_vendor = 'Y'
8 and rownum <= 100
9 order by rcvdate;
V SNUMBER RCVDATE
- ---------- --------------------
A 1 01-jan-2012 00:00:00
P 1 02-jan-2012 00:00:00
A 1 02-jan-2012 00:00:00
P 2 03-jan-2012 00:00:00
A 2 03-jan-2012 00:00:00
A 2 04-jan-2012 00:00:00
i.e.
max(case when vendor = 'A' then 'Y' end) over (partition by snumber) has_vendor
says if there's an 'A' vendor in that snumber, then you want to return that for the "P"arent vendor otherwise not.

Resources