coalesce with subquery in a where clause in Oracle - oracle

Hy,
I have the next sql query in Oracle:
Imagine I have a table "items" with "id" and "name" fields and other table "prices_items" which have three fields named "id, itemId, category". The category may have three values: "1,2,3". So the query I need to do is get the price of an item from the table "prices_items" but the item can have until three prices because of the category field. So, in priotiry order I need to get the price of an item which has category 1, if the item doesnt have this category I have to find the price for category 2 and so on.
from items
left join prices_items on prices_items.itemId = items.itemId
where prices_items.id = coalesce(select id
from prices_items
where itemId= items.itemId and category=1,
select id
from prices_items
where itemId= items.itemId and category=2,
select id
from prices_items
where itemId= items.itemId and category=3)
The query I am using is like this but I dont know how its working because coalesce is being executed on each join?. How is this being executed?
Thanks

The coalesce() is going to keep the first prices_items.id found in order of the categories listed. Instead of individual subqueries you could write it this way and it will probably give a better plan.
select ...
from items inner join prices_items on prices_items.itemId = items.itemId
where prices_items.category = (
select min(pi2.category) from prices_items pi2
where pi2.itemId = items.itemId
);
If the priority of categories doesn't happen to follow an ascending sequence you could handle it with a case expression:
select ...
from items inner join prices_items on prices_items.itemId = items.itemId
where
case prices_items.category
when 2 then 1
when 3 then 2
when 1 then 3
end = (
select
min(case pi2.category
when 2 then 1
when 3 then 2
when 1 then 3
end)
from prices_items pi2
where pi2.itemId = items.itemId
);
As far as how your current query is actually running it may or may not be materializing all the subquery results. From an end results perspective all you really need to know is that only the first non-null value from the coalesce() arguments is the one kept. The reality is that it is probably more efficient to re-write the query so you don't need them.
There are other ways to write this. The one that's most common these days seems to be the row_number() approach:
with data as (
select *,
row_number() over (partition by pi.itemId order by pi.category) as rn
from items inner join prices_items pi on pi.itemId = items.itemId
)
select ...
from data
where rn = 1;
Here's another Oracle-specific solution:
select *
from
items inner join
(
select itemId, min(price) keep (dense_rank first order by category) as price
from prices_items
group by itemId
) pi on pi.itemId = items.itemId;

Oracle Setup:
CREATE TABLE items (
itemid NUMBER PRIMARY KEY,
name VARCHAR2(20)
);
CREATE TABLE prices_items (
itemId NUMBER REFERENCES items ( itemid ),
category INT,
price NUMBER,
CHECK ( category IN ( 1, 2, 3 ) ),
PRIMARY KEY ( itemid, category )
);
INSERT INTO items
SELECT 1, 'A' FROM DUAL UNION ALL
SELECT 2, 'B' FROM DUAL UNION ALL
SELECT 3, 'C' FROM DUAL;
INSERT INTO prices_items
SELECT 1, 1, 32.5 FROM DUAL UNION ALL
SELECT 1, 2, 23.9 FROM DUAL UNION ALL
SELECT 1, 3, 19.99 FROM DUAL UNION ALL
SELECT 2, 1, 42.42 FROM DUAL UNION ALL
SELECT 2, 3, 99.99 FROM DUAL UNION ALL
SELECT 3, 2, 0.02 FROM DUAL UNION ALL
SELECT 3, 3, 10 FROM DUAL;
Query:
SELECT i.itemid,
name,
category,
price
FROM items i
INNER JOIN
( SELECT itemid,
MIN( category ) AS category,
MAX( price ) KEEP ( DENSE_RANK FIRST ORDER BY category ) AS price
FROM prices_items
GROUP BY itemid
) p
ON ( i.itemid = p.itemid );
Output:
ID NAME CATEGORY PRICE
-- ---- -------- -----
1 A 1 32.50
2 B 1 42.42
3 C 2 0.02

Related

How to use GREATEST function with Over Partition by in Oracle

In the below code I want to select customer_name, location, gender and address along with customerid, aread_code.
select
customerid, aread_code, GREATEST(MAX(productid), MAX(itemid))
from
CUSTOMER C
inner join
ORDER O ON c.custid = o.custid
where
c.custtype = 'EXECUTIVE'
group
customerid, by aread_code;
I tried GREATEST function along with OVER PARTITION BY to display required columns. It's throwing an error.
Could you please help me to select the required columns.
Thank you.
DISCLAIMER:
When working with more than one table, qualify the columns with their table name. You haven't done so, so we don't know what of the two tables the aread_code resides in. In my answer here I assume it is the customer's area. If it isn't then you need a different answer.
ANSWER:
You group by customer_id and area code. This gives you one row per customer. And you want the maximum product/item ID from the orders table. (I suppose they are drawn from the same sequence, so you can use this ID somehow to go on from there.)
The easiest approach for this is to get the maximum ID in a subquery. Either directly in the select clause or in the from clause.
Here is how to do this in the SELECT clause:
select
c.*,
(
select greatest(max(productid), max(itemid))
from orders o
where o.custid = c.custid
) as max_id
from customer c
where c.custtype = 'EXECUTIVE';
Here is one way to do this in the FROM clause:
select
c.*,
agg.max_id
from customer c
outer apply
(
select greatest(max(productid), max(itemid)) as max_id
from orders o
where o.custid = c.custid
) agg
where c.custtype = 'EXECUTIVE';
And here is another way to do this in the FROM clause:
select
c.*,
agg.max_id
from customer c
left outer join
(
select
custid,
greatest(max(productid), max(itemid)) as max_id
from orders
group by custid
) agg on agg.custid = c.custid
where c.custtype = 'EXECUTIVE';
If you only want customers with at least one order, then I recommend the approach with the FROM clause. You'd have to turn the OUTER APPLY into a CROSS APPLY resp. the LEFT OUTER JOIN into an INNER JOIN for this.
There are several mistakes in your code. The main confusion is not using table alias prefix for columns. There is a group by mistake and a problem with your table name ORDER - if it is a name of a table. ORDER is a reserved word in Oracle and if it is the name of the table then you should use something like "YOUR_OWNER_NAME"."ORDER".... Here is the corected code with some sample data and result:
WITH
customers (CUSTID, PRODUCTID, AREAD_CODE, CUSTOMER_NAME, LOCATION, GENDER, ADDRESS, CUSTTYPE) AS
(
Select 1, 1, 63, 'Name 1', 'Location 1', 'M', 'Address 1', 'EXECUTIVE' From Dual Union All
Select 2, 1, 63, 'Name 1', 'Location 1', 'M', 'Address 1', 'EXECUTIVE' From Dual Union All
Select 3, 3, 63, 'Name 1', 'Location 1', 'M', 'Address 1', 'EXECUTIVE' From Dual Union All
Select 4, 7, 63, 'Name 1', 'Location 1', 'M', 'Address 1', 'EXECUTIVE' From Dual
),
orders (ORDER_ID, CUSTID, ITEMID, SOME_COLUMN) AS
(
Select 1, 1, 1, 'Some other data' From Dual Union All
Select 2, 2, 1, 'Some other data' From Dual Union All
Select 3, 3, 1, 'Some other data' From Dual Union All
Select 4, 3, 3, 'Some other data' From Dual Union All
Select 5, 4, 1, 'Some other data' From Dual Union All
Select 6, 4, 8, 'Some other data' From Dual
)
select
c.custid, c.aread_code, GREATEST(MAX(c.productid), MAX(o.itemid)) "MAX_ID"
from
CUSTOMERS C
inner join
ORDERS O ON c.custid = o.custid
where
c.custtype = 'EXECUTIVE'
group by
c.custid, c.aread_code
CUSTID AREAD_CODE MAX_ID
---------- ---------- ----------
1 63 1
4 63 8
3 63 3
2 63 1
There are different options to get the rest of the columns depending on your actual data you could use some or all of them.
Option 1 - select and group by as suggested in Beefstu's comment below
select Distinct
c.custid, c.customer_name, c.location, c.address, c.gender, c. custtype, c.aread_code,
GREATEST(MAX(c.productid), MAX(o.itemid)) "MAX_ID"
from
CUSTOMERS C
inner join
ORDERS O ON c.custid = o.custid
where
c.custtype = 'EXECUTIVE'
group by
c.custid, c.customer_name, c.location, c.address, c.gender, c. custtype, c.aread_code
order by c.custid
CUSTID CUSTOMER_NAME LOCATION ADDRESS GENDER CUSTTYPE AREAD_CODE MAX_ID
---------- ------------- ---------- --------- ------ --------- ---------- ----------
1 Name 1 Location 1 Address 1 M EXECUTIVE 63 1
2 Name 1 Location 1 Address 1 M EXECUTIVE 63 1
3 Name 1 Location 1 Address 1 M EXECUTIVE 63 3
4 Name 1 Location 1 Address 1 M EXECUTIVE 63 8
Option 2. - using analytic functions MAX() OVER() with Distinct keyword (could be performance costly with big datasets) - result is the same as above
select Distinct
c.custid, c.customer_name, c.location, c.address, c.gender, c. custtype, c.aread_code,
GREATEST(MAX(c.productid) OVER(Partition By c.custid), MAX(o.itemid) OVER(Partition By c.custid)) "MAX_ID"
from
CUSTOMERS C
inner join
ORDERS O ON c.custid = o.custid
where
c.custtype = 'EXECUTIVE'
order by c.custid
Option 3 - using left join to a subquery - see the solution offered by Thorsten Kettner

Multiply with Previous Value from One colum in Oracle SQL

I have the following result, which is easily calculated in Excel, but how to do it in Oracle, the result is the following, based on a previous select and comes from one column,
Result from select Expected result
1.62590
0.60989 0.991620151
0.83859 0.831562742
the result is based on 1.62590 * 0.60989 = 0.991620151,
1.62590 * 0.60989 * 0.83859 = 0.831562742
You can use:
SELECT id,
result,
EXP(SUM(LN(result)) OVER (ORDER BY id)) AS expected
FROM table_name;
Note: Use any other column instead of id to give the appropriate ordering or, if your rows are already ordered, use the ROWNUM pseudo-column instad of id.
Which, for the sample data:
CREATE TABLE table_name (id, Result) AS
SELECT 1, 1.62590 FROM DUAL UNION ALL
SELECT 2, 0.60989 FROM DUAL UNION ALL
SELECT 3, 0.83859 FROM DUAL;
Outputs:
ID
RESULT
EXPECTED
1
1.6259
1.62590000000000000000000000000000000001
2
.60989
.9916201510000000000000000000000000000026
3
.83859
.8315627424270900000000000000000000000085
fiddle
One option is to use a recursive CTE; it, though, expects that sample data can be sorted, somehow, so I added the ID column which starts with 1, while other values are incremented by 1:
Sample data:
SQL> with
2 test (id, col) as
3 (select 1, 1.62590 from dual union all
4 select 2, 0.60989 from dual union all
5 select 3, 0.83859 from dual
6 ),
Query begins here:
7 product (id, col, prod) as
8 (select id, col, col
9 from test
10 where id = 1
11 union all
12 select t.id, t.col, t.col * p.prod
13 from test t join product p on p.id + 1 = t.id
14 )
15 select id,
16 round(prod, 10) result
17 from product;
ID RESULT
---------- ----------
1 1,6259
2 ,991620151
3 ,831562742
SQL>
You can use a MODEL clause:
SELECT *
FROM (SELECT ROW_NUMBER() OVER (ORDER BY id) AS rn, result FROM table_name)
MODEL
DIMENSION BY (rn)
MEASURES ( result, 0 AS expected)
RULES (
expected[rn] = result[cv()] * COALESCE(expected[cv()-1], 1)
)
order by rn;
Which, for the sample data:
CREATE TABLE table_name (id, Result) AS
SELECT 1, 1.62590 FROM DUAL UNION ALL
SELECT 2, 0.60989 FROM DUAL UNION ALL
SELECT 3, 0.83859 FROM DUAL;
Outputs:
RN
RESULT
EXPECTED
1
1.6259
1.6259
2
.60989
.991620151
3
.83859
.83156274242709
fiddle

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

Count(*) in Group By - Returns 1, instead of '0'

I am using the below oracle query to get the count of rows.
SELECT T.ID,T.NAME,COUNT(*) AS NO_OF_STUDENTS FROM STUDENT S RIGHT JOIN
TEACHER T ON S.TEACHER_ID = T.ID
GROUP BY T.ID,T.NAME ORDER BY T.ID
Actual Result Should be:
TEACHER 1 - 10 STUDENTS
TEACHER 2 - 5 STUDENTS
TEACHER 3 - 0 STUDENT
The Result what i am getting is:
TEACHER 1 - 10 STUDENTS
TEACHER 2 - 5 STUDENTS
TEACHER 3 - 1 STUDENT
Since TEACHER 3 is not having any student, the result should be 0 Student. But i am getting the result as 1 Student.
You need to count a specific column (not use *, which includes nulls) in the table being outer joined to - since that is the one that might not have matching data. So:
SELECT T.ID, T.NAME, COUNT(S.ID) AS NO_OF_STUDENTS
FROM STUDENT S
RIGHT JOIN TEACHER T ON S.TEACHER_ID = T.ID
GROUP BY T.ID, T.NAME
ORDER BY T.ID
The only difference is COUNT(S.ID) instead of COUNT(*).
Simple demo with made-up data provided via CTEs:
with teacher (id, name) as (
select 1, 'Teacher 1' from dual
union all select 2, 'Teacher 2' from dual
union all select 3, 'Teacher 3' from dual
),
student (id, teacher_id) as (
select level, 1 from dual connect by level <= 10
union all
select level + 10, 2 from dual connect by level <= 5
)
SELECT T.ID, T.NAME, COUNT(S.ID) AS NO_OF_STUDENTS
FROM STUDENT S
RIGHT JOIN TEACHER T ON S.TEACHER_ID = T.ID
GROUP BY T.ID, T.NAME
ORDER BY T.ID;
ID NAME NO_OF_STUDENTS
---------- --------- --------------
1 Teacher 1 10
2 Teacher 2 5
3 Teacher 3 0
You could also do this as a left join, which I find more intuitive:
SELECT T.ID, T.NAME, COUNT(S.ID) AS NO_OF_STUDENTS
FROM TEACHER T
LEFT JOIN STUDENT S ON S.TEACHER_ID = T.ID
GROUP BY T.ID, T.NAME
ORDER BY T.ID
which gets the same result.

Oracle Hirerachical Query [duplicate]

This question already exists:
Oracle Hierarchical Query
Closed 8 years ago.
I want to write oracle hirerachical query which would return parent value if child has no value.If child has value it should return child value only. If child has no value it has to traverse until its one parent has value.
Select * From (
Select Orgid,Text
From (
Select Cusid,parent_id, Orgid, Rownum R
FROM (
SELECT cus.customerid cusid, org.Id orgid,
cus.customername, org.parent_id
From Boms_Customer Cus, Boms_Organization Org
Where Cus.Organization_Id=Org.Id
) A
START WITH a.cusid=100
Connect By Prior A.Parent_Id = A.Orgid
) X,
(
Select Org.Organization_Id,Org.Text
From FORTRESS.SUPPORT_QUESTION org
) Y
WHERE x.orgid = y.ORGANIZATION_ID ORDER BY x.r
)
But my query is returning all parents data and child data.Please help me inquery getting only childrens data if it has data otherwise it should return parents data.
Well, with a bit of work here is what I get:
with organisation(id, parent_id, name) as
(
select 1, null, 'Cisco' from dual
union all
select 2, 1, 'vodafone' from dual
union all
select 22, 1, 'SFR' from dual
union all
select 3, 1, 'Airtel' from dual
union all
select 4, 2, 'Aircel' from dual
),
question(ID, ORG_ID, QName) as
(
select 1, 1, 'Test' from dual
union all
select 2, 1, 'XYZ' from dual
union all
select 3, 2, 'Testing Network' from dual
),
have_question(org_id) as
(
select distinct org_id from question
),
tree as
(
select o.*, level lev, sys_connect_by_path(id, '/') path
from organisation o
connect by parent_id = prior id
start with parent_id is null
),
ancestors as
(
select t.id, t2.id ancestor, t2.lev ancestor_lev
from tree t, tree t2, have_question h
where t.path || '/' like t2.path || '/%'
and h.org_id (+) = t2.id
and h.org_id is not null
)
select a.id, max(ancestor) keep (dense_rank last order by ancestor_lev) first_ancestor_with_question
from ancestors a
group by a.id
order by 1
;
You only have to do a join with question table to get the corresponding questions.

Resources