Get parent path of hierarchical data in oracle - sql

I have three tables, CATEGORY, GROUPING and PERFORMER where a category's direct children could be any other category or grouping or performer and a grouping's children could be any other grouping or performer, given this context when a category id or grouping id or performer id is provided then I need to get the whole parent path of given id. How to get it using SQL in oracle
if performer_id= 300 then result should be 300->202->201->101->100
if grouping_id = 203 then result should be 203->102->101->100
if category_id = 103 then result should be 103->101->100

Stack overflow is not a coding site, you should always include examples of what you have tried. That said, I found this so intriguing, I thought I would give it a try myself.
This is a totally brute force method. I extract the hierarchies from each level, then use listagg to bring them together:
WITH
category
AS
(SELECT '100' category_id, NULL parent_id
FROM DUAL
UNION ALL
SELECT '101' category_id, '100' parent_id
FROM DUAL
UNION ALL
SELECT '102' category_id, '101' parent_id
FROM DUAL
UNION ALL
SELECT '103' category_id, '101' parent_id
FROM DUAL),
GROUPING
AS
(SELECT '200' GROUPING_ID, NULL parent_id, '101' category_id
FROM DUAL
UNION ALL
SELECT '201' GROUPING_ID, '200' parent_id, '101' category_id
FROM DUAL
UNION ALL
SELECT '202' GROUPING_ID, '201' parent_id, '101' category_id
FROM DUAL
UNION ALL
SELECT '203' GROUPING_ID, NULL parent_id, '102' category_id
FROM DUAL),
performer
AS
(SELECT '300' performer_id, '202' GROUPING_ID, '101' category_id
FROM DUAL
UNION ALL
SELECT '301' performer_id, '201' GROUPING_ID, '101' category_id
FROM DUAL
UNION ALL
SELECT '302' performer_id, '203' GROUPING_ID, '103' category_id
FROM DUAL
UNION ALL
SELECT '303' performer_id, NULL GROUPING_ID, '102' category_id
FROM DUAL),
pset (p_gid, p_parentid, p_catid)
AS
(SELECT GROUPING.GROUPING_ID, parent_id, GROUPING.category_id
FROM performer INNER JOIN GROUPING ON performer.GROUPING_ID = GROUPING.GROUPING_ID
WHERE performer_id = '300'
UNION ALL
SELECT GROUPING_ID, parent_id, category_id
FROM pset INNER JOIN GROUPING ON GROUPING_ID = p_parentid),
cset (p_catid, p_parent)
AS
(SELECT p_catid, parent_id
FROM pset INNER JOIN category ON pset.p_catid = category.category_id
UNION ALL
SELECT category_id, parent_id
FROM cset INNER JOIN category ON category_id = p_parent),
dset
AS
(SELECT p_catid
FROM cset
UNION
SELECT p_gid
FROM pset
UNION
SELECT '300'
FROM DUAL)
SELECT LISTAGG (p_catid, '->') WITHIN GROUP (ORDER BY p_catid DESC) AS performer_chain
FROM dset
And the answer is
PERFORMER_CHAIN
300->202->201->200->101->100

Related

unpivot query join to other tables

I have a query like below
with t as (
select ID, name, tag, tag_1, tag_2, tag_3, tag_4, location from table_one
)
select * from t
unpivot (
value for _tag_ in (tag,tag_1,tag_2,tag_3,tag_4)
)
Now, I want to join 3 other tables table1, table2, table3 to the above, I need to select other columns example col1, col2, col3 from those tables. Any idea on how to proceed with that.
I would use a lateral join in Oracle 12C+:
select u.*
from t cross apply
(select id, name, tag from dual union all
select id, name, tag_1 from dual union all
select id, name, tag_2 from dual union all
select id, name, tag_3 from dual union all
select id, name, tag_4 from dual
) u;
You can then join to u as you would anything else:
select u.*, . . .
from t cross apply
(select id, name, tag from dual union all
select id, name, tag_1 from dual union all
select id, name, tag_2 from dual union all
select id, name, tag_3 from dual union all
select id, name, tag_4 from dual
) u join
x
on u.? = x.?;
In Oracle 11, you can do something similar if you make the unpivot a subquery or CTE.

print name of parents who have children with same name

PARENT table PK is PID and PID is FK in CHILDREN table. How do I print names of Parents from PARENT table that have children who share the same name as another child in CHILDREN table? I think a recursive join should be used to find the same name but I can't get it to work. I am able to join the PARENT and CHILDREN tables using below query:
select PARENT.NAME as ParentName
from PARENT inner join CHILDREN
on PARENT.PID=CHILDREN.PID
group by NAME;
I have tried this query to complete the recursive join but it isn't working:
select CHILDREN.NAME
from CHILDREN e, CHILDREN m
where e.CHILDREN.PID=m.CHILDREN.PID
order by CHILDREN.PID;
Group by child name and evaluate, if at least two different parents exist.
-- TEST DATA
with parent(pid, name) as
(select 1, 'Parent1' from dual
union all
select 2, 'Parent2' from dual
union all
select 3, 'Parent3' from dual
union all
select 4, 'Parent4' from dual),
children(name, pid) as
(select 'Tom', 1 from dual
union all
select 'Tim', 1 from dual
union all
select 'Steven', 2 from dual
union all
select 'Tim', 2 from dual
union all
select 'Marta', 2 from dual
union all
select 'Jess', 3 from dual
union all
select 'Jim', 4 from dual
union all
select 'Jess', 4 from dual)
--> SELECT
select c.name, listagg(p.name, ',') within group(order by p.name)
from parent p
join children c
on c.pid = p.pid
group by c.name -- group by child name
having min (p.pid) <> max (p.pid) -- at least two different parents
--> RESULT
Jess Parent3,Parent4
Tim Parent1,Parent2
AND Parent.Name = Children.Name
So wouldn't this print names of the parents where it matches with Children's names?

Multiple columns for the same WHERE IN set

Let's suppose I want to find out all the people who are either parent or child of an specific set of people.
I could do something like:
SELECT *
FROM people P
WHERE
P.parent_id IN ('111', 'abc', '42', '1a2b3c') OR
P.child_id IN ('111', 'abc', '42', '1a2b3c')
Is there any way in which I could avoid writing the list twice (or more times if I were looking for more columns)?
I'm looking for something like:
(...) WHERE (P.parent_id OR P.child_id) IN ('111', 'abc', '42', '1a2b3c')
I'm using Oracle, but a plain SQL solution would be appreciated too.
Try this:
WITH search_ids (id) AS (
SELECT '111' FROM dual
UNION ALL SELECT 'abc' FROM dual
UNION ALL SELECT '42' FROM dual
UNION ALL SELECT '1a2b3c' FROM dual
)
SELECT * FROM people P
WHERE P.parent_id IN (SELECT id FROM search_ids)
OR P.child_id IN (SELECT id FROM search_ids)
;
The FROM dual bit is Oracle specific.
Happy playing
Marco
In terms of performance and universality, it's better to use the other approach than OR for such a case. In Oracle, there are SET operators which could help you very much. For example for your case, your query could look like this:
select *
from people p
where
exists (
(
select p.parent_id from dual
union all
select p.child_id from dual
) intersect (
select '111' from dual
union all
select 'abc' from dual
union all
select '42' from dual
union all
select '1a2b3c' from dual
)
)
Or using with clause:
with people_list (value) as (
select '111' from dual
union all
select 'abc' from dual
union all
select '42' from dual
union all
select '1a2b3c' from dual
)
select * from people p
where
exists (
(
select p.parent_id from dual
union all
select p.child_id from dual
) intersect (
select value from people_list
)
)
Benefits of such an approach:
It is better in terms of performance (as it was already mentioned, it is hard for optimizer to digest ORs in queries, especially when they are a bit more complex).
It is more universal - indeed, you can add as many columns as you want to be checked if they are in the given set of values.
Instead of selecting from dual, you can use any existing table in your schema.
This subquery integrates much easier into a complex query (and more efficiently as I already mentioned in the first point).
You are already using OR, so the query is hard to optimize. Hence, regular expressions are an option:
SELECT *
FROM people P
WHERE regexp_like('[' || P.parent_id || '][' || p.child_id || ']') regexp_like('\[111|abc|42|1a2b3c\]')
One way is with a hierarchical query (and then it's more general - you can use different cutoffs by LEVEL):
select *
from people
connect by level <= 2
and parent_id = prior child_id
start with child_id in ( ..... )
It would still be best to have the "search id's" in a separate table, and the IN condition as in (select search_id from helper_table) as shown in another answer.
CREATE TABLE #People(parent_id NVARCHAR(50),child_id NVARCHAR(50))
GO
INSERT INTO #People
( parent_id, child_id )
VALUES ( N'111', -- parent_id - nvarchar(50)
N'321331' -- child_id - nvarchar(50)
),( N'111', -- parent_id - nvarchar(50)
N'abc' -- child_id - nvarchar(50)
),( N'42', -- parent_id - nvarchar(50)
N'321331' -- child_id - nvarchar(50)
),( N'111', -- parent_id - nvarchar(50)
N'1a2b3c' -- child_id - nvarchar(50)
),( N'11dsdfs1', -- parent_id - nvarchar(50)
N'1a2sdfsdfsb3c' -- child_id - nvarchar(50)
)
;WITH CTE (Value) AS (
SELECT '111'
UNION SELECT 'abc'
UNION SELECT '42'
UNION SELECT '1a2b3c'
)
SELECT *
FROM #People p
WHERE EXISTS(
(SELECT p.parent_id
UNION
SELECT p.child_id
)
INTERSECT
SELECT value
FROM CTE
)
You can probably group them using something like below [Brevity: idea taken from SQL multiple columns in IN clause
WHERE (P.parent_id, P.child_id) IN (('111','111'), ('abc','abc'),('42','42'), ('1a2b3c','1a2b3c'));

Oracle Pivot on column larger than 4000 characters

select * from
(
select id, type, LISTAGG(value,',') WITHIN GROUP (ORDER BY 1) as value from
(
select 'user1' as id, 'BMW' as value, 'CAR' as type from dual union
select 'user1' as id, 'Audi' as value, 'CAR' as type from dual union
select 'user2' as id, 'Honda' as value, 'CAR' as type from dual union
select 'user1' as id, 'Dell' as value, 'COMPUTER' as type from dual union
select 'user1' as id, 'Sony' as value, 'COMPUTER' as type from dual union
select 'user2' as id, 'HP' as value, 'COMPUTER' as type from dual
)
group by id, type
)
PIVOT (max(value) FOR id IN ('user1' user1, 'user2' user2));
Is there a way of using the above example if the LISTAGG is to be larger than 4000 characters?
Pivot doesn't seem to work on a CLOB column. I tried to use XMLAGG with a .getClobVAl() around it. This gives me a list larger than 4000 but cannot pivot.

SQL add examples from grouping data

Hello I have a small question in oracle SQL.
I have table Auto_Parts:
Category,Manufacturer_id,Part_name
Tires,Michelin, Pilot Pro
Tires,Michelin, Power One
Tires,Bridgestone, Potenza
Tires,Bridgestone, Turanza
Tires,Bridgestone, Blizzak
The query:
select Category,Manufacturer_id,count(*) cnt,example_1,example_2,example_3
from auto_parts
group by Category,Manufacturer_id
result:
Category,Manufacturer_id,cnt ,example_1,example_2,example_3
Tires ,Michelin ,1000 ,Pilot Pro,Power One,Power Two
Tires ,Bridgestone ,200 ,Potenza ,Turanza ,Blizzak
Question: how can I get 3 arbitrary values from the table above and present them as 3 columns in my query (a sample output is presented above, the columns are example_1,2,3)
This should do the trick. Obviously you don't need the WITH block, I just used that to mimic your data, so your query would start at the "select Category..."
with auto_parts as
(
select 'Tires' as Category,'Michelin' as Manufacturer_id, 'Pilot Pro' as part_name from dual union all
select 'Tires' as Category,'Michelin' as Manufacturer_id, 'Power One' as part_name from dual union all
select 'Tires' as Category,'Bridgestone' as Manufacturer_id, 'Potenza' as part_name from dual union all
select 'Tires' as Category,'Bridgestone' as Manufacturer_id, 'Turanza' as part_name from dual union all
select 'Tires' as Category,'Bridgestone' as Manufacturer_id, 'Blizzak' as part_name from dual
)
select
Category
Manufacturer_id,
count(*) cnt ,
max(case when rn = 1 then part_name end) example_1,
max(case when rn = 2 then part_name end) example_2,
max(case when rn = 3 then part_name end) example_3
from (
select category, manufacturer_id, part_name, row_number() over (partition by category, manufacturer_id order by dbms_random.value) rn
from auto_parts
)
group by Category,Manufacturer_id;