Oracle Hirerachical Query [duplicate] - oracle

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.

Related

Oracle ordering with IN clause [duplicate]

Is it possible to keep order from a 'IN' conditional clause?
I found this question on SO but in his example the OP have already a sorted 'IN' clause.
My case is different, 'IN' clause is in random order
Something like this :
SELECT SomeField,OtherField
FROM TestResult
WHERE TestResult.SomeField IN (45,2,445,12,789)
I would like to retrieve results in (45,2,445,12,789) order. I'm using an Oracle database. Maybe there is an attribute in SQL I can use with the conditional clause to specify to keep order of the clause.
There will be no reliable ordering unless you use an ORDER BY clause ..
SELECT SomeField,OtherField
FROM TestResult
WHERE TestResult.SomeField IN (45,2,445,12,789)
order by case TestResult.SomeField
when 45 then 1
when 2 then 2
when 445 then 3
...
end
You could split the query into 5 queries union all'd together though ...
SELECT SomeField,OtherField
FROM TestResult
WHERE TestResult.SomeField = 4
union all
SELECT SomeField,OtherField
FROM TestResult
WHERE TestResult.SomeField = 2
union all
...
I'd trust the former method more, and it would probably perform much better.
Decode function comes handy in this case instead of case expressions:
SELECT SomeField,OtherField
FROM TestResult
WHERE TestResult.SomeField IN (45,2,445,12,789)
ORDER BY DECODE(SomeField, 45,1, 2,2, 445,3, 12,4, 789,5)
Note that value,position pairs (e.g. 445,3) are kept together for readability reasons.
Try this:
SELECT T.SomeField,T.OtherField
FROM TestResult T
JOIN
(
SELECT 1 as Id, 45 as Val FROM dual UNION ALL
SELECT 2, 2 FROM dual UNION ALL
SELECT 3, 445 FROM dual UNION ALL
SELECT 4, 12 FROM dual UNION ALL
SELECT 5, 789 FROM dual
) I
ON T.SomeField = I.Val
ORDER BY I.Id
There is an alternative that uses string functions:
with const as (select ',45,2,445,12,789,' as vals)
select tr.*
from TestResult tr cross join const
where instr(const.vals, ','||cast(tr.somefield as varchar(255))||',') > 0
order by instr(const.vals, ','||cast(tr.somefield as varchar(255))||',')
I offer this because you might find it easier to maintain a string of values rather than an intermediate table.
I was able to do this in my application using (using SQL Server 2016)
select ItemID, iName
from Items
where ItemID in (13,11,12,1)
order by CHARINDEX(' ' + Convert("varchar",ItemID) + ' ',' 13 , 11 , 12 , 1 ')
I used a code-side regex to replace \b (word boundary) with a space. Something like...
var mylist = "13,11,12,1";
var spacedlist = replace(mylist,/\b/," ");
Importantly, because I can in my scenario, I cache the result until the next time the related items are updated, so that the query is only run at item creation/modification, rather than with each item viewing, helping to minimize any performance hit.
Pass the values in via a collection (SYS.ODCINUMBERLIST is an example of a built-in collection) and then order the rows by the collection's order:
SELECT t.SomeField,
t.OtherField
FROM TestResult t
INNER JOIN (
SELECT ROWNUM AS rn,
COLUMN_VALUE AS value
FROM TABLE(SYS.ODCINUMBERLIST(45,2,445,12,789))
) i
ON t.somefield = i.value
ORDER BY rn
Then, for the sample data:
CREATE TABLE TestResult ( somefield, otherfield ) AS
SELECT 2, 'A' FROM DUAL UNION ALL
SELECT 5, 'B' FROM DUAL UNION ALL
SELECT 12, 'C' FROM DUAL UNION ALL
SELECT 37, 'D' FROM DUAL UNION ALL
SELECT 45, 'E' FROM DUAL UNION ALL
SELECT 100, 'F' FROM DUAL UNION ALL
SELECT 445, 'G' FROM DUAL UNION ALL
SELECT 789, 'H' FROM DUAL UNION ALL
SELECT 999, 'I' FROM DUAL;
The output is:
SOMEFIELD
OTHERFIELD
45
E
2
A
445
G
12
C
789
H
fiddle

Query to join Oracle tables

I have a 'MASTER' table as shown below:
Entity Cat1 Cat2
A Mary;Steve Jacob
B Alex;John Sally;Andrew
Another table 'PERSON' has associations of person's name (this could be InfoID as well) with emails.
Name Email InfoID
Mary maryD#gmail.com mryD
Steve steveR#gmail.com stvR
Jacob jacobB#gmail.com jacbb
Sally sallyD#gmail.com sallD
Alex AlexT#gmail.com alexT
John JohnP#gmail.com johP
Andrew AndrewV#gmail.com andV
I want to join the person table with master such as:
Entity Cat1 EmailCat1 Cat2 EmailCat2
A Mary;Steve maryD#gmail.com;steveR#gmail.com Jacob jacobB#gmail.com
B Alex;John AlexT#gmail.com;JohnP#gmail.com Sally;Andrew sallyD#gmail.com;AndrewV#gmail.com
any insights on how to go about it?
Honestly, your master table design needs to be normalized. But, in the meantime you could try this query below :
with
needed_rows_for_cat1_tab (lvl) as (
select level from dual
connect by level <= (select max(regexp_count(Cat1, ';')) from Your_bad_master_tab) + 1
)
, needed_rows_for_cat2_tab (lvl) as (
select level from dual
connect by level <= (select max(regexp_count(Cat2, ';')) from Your_bad_master_tab) + 1
)
, split_cat1_val_tab as (
select Entity, Cat1
, substr(Cat1||';'
, lag(pos, 1, 0)over(partition by Entity order by lvl) + 1
, pos - lag(pos, 1, 0)over(partition by Entity order by lvl) - 1
) val
, lvl
, pos
, 1 cat
from (
select Entity, Cat1, instr(Cat1||';', ';', 1, r1.lvl)pos, r1.lvl
from Your_bad_master_tab c1
join needed_rows_for_cat1_tab r1 on r1.lvl <= regexp_count(Cat1, ';') + 1
)
)
, split_cat2_val_tab as (
select Entity, Cat2
, substr(Cat2||';'
, lag(pos, 1, 0)over(partition by Entity order by lvl) + 1
, pos - lag(pos, 1, 0)over(partition by Entity order by lvl) - 1
) val
, lvl
, pos
, 2 cat
from (
select Entity, Cat2, instr(Cat2||';', ';', 1, r2.lvl)pos, r2.lvl
from Your_bad_master_tab c1
join needed_rows_for_cat2_tab r2 on r2.lvl <= regexp_count(Cat2, ';') + 1
)
)
select ENTITY
, max(decode(cat, 1, CAT1, null)) CAT1
, listagg(decode(cat, 1, EMAIL, null), ';')within group (order by lvl) EmailCat1
, max(decode(cat, 2, CAT1, null)) CAT2
, listagg(decode(cat, 2, EMAIL, null), ';')within group (order by lvl) EmailCat2
from (
select c.*, p.Email
from split_cat1_val_tab c join Your_person_tab p on (c.val = p.name)
union all
select c.*, p.Email
from split_cat2_val_tab c join Your_person_tab p on (c.val = p.name)
)
group by ENTITY
;
Here are your sample data
--drop table Your_bad_master_tab purge;
create table Your_bad_master_tab (Entity, Cat1, Cat2) as
select 'A', 'Mary;Steve', 'Jacob' from dual union all
select 'B', 'Alex;John', 'Sally;Andrew' from dual
;
--drop table Your_person_tab purge;
create table Your_person_tab (Name, Email, InfoID) as
select 'Mary', 'maryD#gmail.com' ,'mryD' from dual union all
select 'Steve', 'steveR#gmail.com' ,'stvR' from dual union all
select 'Jacob', 'jacobB#gmail.com' ,'jacbb' from dual union all
select 'Sally', 'sallyD#gmail.com' ,'sallD' from dual union all
select 'Alex', 'AlexT#gmail.com' ,'alexT' from dual union all
select 'John', 'JohnP#gmail.com' ,'johP' from dual union all
select 'Andrew', 'AndrewV#gmail.com' ,'andV' from dual
;
DB<>fiddle

sql placeholder rows

I have an apex item P_USERS which can have a value higher than the amount of rows returning from the query below.
I have a classic report which has the following query:
select
first_name,
last_name
from accounts
where account_role = 'Author'
order by account_nr;
I want placeholder rows to be added to the query (first_name = null, last_name = null etc.), if the total rows from the query is lesser than the value in the apex_item P_USERS.
Any tips on how to achieve this? Maybe with a LEFT join?
If you have more result than the minima you defined, you must add the rest with union.
Here is what you could try to adapt to your case:
SELECT i,c FROM (
select rownum i, c from (
select 'a' c from dual union all select 'b' from dual union all select 'd' from dual union all select 'be' from dual
)), (Select Rownum r From dual Connect By Rownum <= 3)
where (i(+)= r)
union select i,c from (select rownum i, c from (
select 'a' c from dual union all select 'b' from dual union all select 'd' from dual union all select 'be' from dual
)) where i>3
You may try to use a LEFT JOIN.
First, create a list of number until the limit you want like suggested here:
-- let's say you want 300 records
Select Rownum r From dual Connect By Rownum <= 300
Then you can use this to left join and have empty records:
SELECT C, R FROM
( select rownum i, c from (select 'a' c from dual union all select 'b' from dual) )
, ( Select Rownum r From dual Connect By Rownum <= 300)
where i(+)= r order by r
The above gives you an ordered list starting with 'a', 'b', then null until the end.
So you could adapt it to your case so:
SELECT F,L FROM
( select rownum i, f, l from (
select first_name f, last_name l
from accounts where account_role = 'Author'
order by account_nr) )
, ( Select Rownum r From dual Connect By Rownum <= 300)
where i(+)= r

coalesce with subquery in a where clause in 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

Group 'n' rows in to columns - oracle

I have a situation where I need to split 'n' rows in to column group. For example, Below is dataset
COMMENT_TEXT
T1
T2
T3
T4
T5
T6
Expected Output:
SUN MON TUE
T1 T2 T3
T4 T5 T6
My Query:
SELECT htbp1.comment_text
FROM hxc_time_building_blocks htbp,
hxc_time_building_blocks htbp1
WHERE htbp1.parent_building_block_id = htbp.time_building_block_id
AND htbp1.parent_building_block_ovn = htbp.parent_building_block_ovn
AND htbp.parent_building_block_id = 116166
AND htbp.parent_building_block_ovn = 1
ORDER BY htbp1.time_building_block_id
Is there any way I can do PIVOT with a 'n' rows and without aggregate function?
Edit: T1/T2/T3 as sample data sets but in real it can be any random free text or null.
SELECT * FROM (SELECT htbp1.comment_text, TO_CHAR (htbp.start_time, 'DY') par_time,
trunc((rownum-1) / 7) buck
FROM hxc_time_building_blocks htbp,
hxc_time_building_blocks htbp1,
hxc_timecard_summary hts
WHERE hts.RESOURCE_ID = :p_resource_id
AND TRUNC(hts.STOP_TIME) = TRUNC(:p_wkend_date)
AND htbp1.parent_building_block_id = htbp.time_building_block_id
AND htbp1.parent_building_block_ovn = htbp.parent_building_block_ovn
AND htbp.parent_building_block_id = hts.timecard_id
AND htbp.parent_building_block_ovn = hts.timecard_ovn
ORDER BY htbp1.time_building_block_id ) PIVOT( max(comment_text) FOR par_time
IN ('SUN' AS "SUN",
'MON' AS "MON",
'TUE' AS "TUE",
'WED' AS "WED",
'THU' AS "THU",
'FRI' AS "FRI",
'SAT' AS "SAT"));
When I added the another table 'hxc_timecard_summary' which is parent then data is going crazy, but if I use the hardcoded parameters like the one in the first then the rows are showing up fine.
PIVOT also uses an aggregate function but you don't need a GROUP BY:
with tab as (
select sysdate - 7 date_col, 'T1' comment_text from dual
union all select sysdate - 6, 'T2' from dual
union all select sysdate - 5, 'T3' from dual
union all select sysdate - 4, 'T4' from dual
union all select sysdate - 3, 'T5' from dual
union all select sysdate - 2, 'T6' from dual
union all select sysdate - 1, 'T7' from dual
)
select * from (select to_char(date_col, 'D') day_of_week, comment_text from tab)
PIVOT (max(comment_text) for day_of_week in (7 as sun, 1 as mon, 2 as tue));
Also, I suppose you need the second column with a date to form your new columns.
And you cannot use expressions for FOR clause - this should be column(s). For example, this won't work:
select * from tab
PIVOT (max(comment_text) for to_char(date_col, 'D') in (7 as sun, 1 as mon, 2 as tue));
because of to_char(date_col, 'D')
Try using pivot.
It allows rows to be mapped to columns.
Its from 11g onwards I believe.
with tab as (
select 'T1' comment_text from dual
union all select 'T2' from dual
union all select 'T3' from dual
union all select 'T4' from dual
union all select 'T5' from dual
union all select 'T6' from dual
)
select regexp_substr(txt, '[^,]+', 1, 1) sun,
regexp_substr(txt, '[^,]+', 1, 2) mon,
regexp_substr(txt, '[^,]+', 1, 3) tue
from (
select buck, wm_concat(comment_text) txt
from (
select comment_text, trunc((rownum-1) / 3) buck
from (select comment_text from tab order by comment_text)
)
group by buck
);
wm_concat(comment_text) (Oracle 10g) =
listagg(comment_text, ',') within group(order by comment_text) (Oracle 11g)
But these two functions are both aggregate
My third try, no aggregate functions at all (works fine in Oracle 10g)
with tab as (
select 'T1' comment_text from dual
union all select 'T2' from dual
union all select 'T3' from dual
union all select 'T4' from dual
union all select 'T5' from dual
union all select 'T6' from dual
)
select regexp_substr(txt, '[^(#####)]+', 1, 1) sun,
regexp_substr(txt, '[^(#####)]+', 1, 2) mon,
regexp_substr(txt, '[^(#####)]+', 1, 3) tue
from (
select sys_connect_by_path(comment_text, '#####') txt, parent_id
from (
select rownum id, comment_text, mod(rownum-1, 3) parent_id
from (select comment_text from tab order by comment_text)
)
start with parent_id = 0
connect by prior id = parent_id
) where parent_id = 2;

Resources