Oracle Hierarchy to get all children and all parents of each id - sql

I have a table with parent/child ids, and I'm trying to get a full list of all levels of parents AND children for a given id.
Basically, for a given id, go all the way down and all the way up the hierarchy.
I've tried connect by, but maybe a recursive CTE would be better?
select 'abc' as child, null as parent from dual union all
select 'mno' as child, 'abc' as parent from dual union all
select 'def' as child, 'abc' as parent from dual union all
select '123' as child, 'abc' as parent from dual union all
select 'qrs' as child, '123' as parent from dual union all
select '789' as child, 'def' as parent from dual union all
select 'xyz' as child, '123' as parent from dual
Ex:
Child
Parent
abc
null
mno
abc
def
abc
123
abc
qrs
123
789
def
xyz
123
For 123, the desired output:
xyz > 123 > abc
qrs > 123 > abc
For abc, the desired output:
xyz > 123 > abc
789 > def > abc
qrs > 123 > abc
mno > abc
Here's my attempt. It seems kind of hacky with the full_hier being a concatenation + substring of the child & parent paths. Plus, I'm getting additional results that I'm not sure how to filter out (Ex: def > abc is returned though I don't want it as it's captured in 789 > def > abc).
select
connect_by_root child,
substr(sys_connect_by_path(child, '>' ),2) as child_hier
, substr(sys_connect_by_path(parent, '>' ),2) as parent_hier
, case
when parent is null then substr(sys_connect_by_path(child, '>' ),2)
else substr(sys_connect_by_path(child, '>' ),2) || substr(substr(sys_connect_by_path(parent, '>' ),2), instr(substr(sys_connect_by_path(parent, '>' ),2),'>',1,1))
end as full_hier
, level
from
(
select 'abc' as child, null as parent from dual union all
select 'mno' as child, 'abc' as parent from dual union all
select 'def' as child, 'abc' as parent from dual union all
select '123' as child, 'abc' as parent from dual union all
select 'qrs' as child, '123' as parent from dual union all
select '789' as child, 'def' as parent from dual union all
select 'xyz' as child, '123' as parent from dual
) table_name
where 1=1
--and connect_by_isleaf = 1
--and connect_by_root child in ('123')
and child = 'abc'
connect by child = prior parent
--connect_by prior parent = child
Thanks for taking a look, I appreciate it!

Another method.
This time via recursive CTE's.
with cte_init (base) as (
select '123' as base
from dual
),
rcte_hierarchy_down (base, lvl, child, parent) as (
select
child as base
, 0 as lvl
, child
, parent
from test_hierarchy
where child in (select base from cte_init)
union all
select
cte.base
, cte.lvl-1
, t.child
, t.parent
from rcte_hierarchy_down cte
join test_hierarchy t
on t.child = cte.parent
),
rcte_hierarchy_up (lvl, child, parent, path) as (
select
1 as lvl
, child
, parent
, child||'>'||parent as path
from test_hierarchy h
where parent in (select child
from rcte_hierarchy_down
where parent is null)
union all
select
cte.lvl+1
, t.child
, t.parent
, t.child||'>'||cte.path
from rcte_hierarchy_up cte
join test_hierarchy t
on t.parent = cte.child
)
select distinct h.path
from rcte_hierarchy_up h
join cte_init i on h.path like '%'||i.base||'%'
and not exists (
select 1
from test_hierarchy t
where t.parent = h.child
)
PATH
qrs>123>abc
xyz>123>abc
Demo on db<>fiddle here

If I understand correctly, given any id (as an input - below I use a bind variable in the query), you need to find all its leaf descendants, and then for each such leaf, show the full path from leaf to the root of the hierarchy.
One way to do that is to traverse the hierarchy twice: first start from the given id and find all its leaf descendants, then traverse in the opposite direction to find all the "full paths".
While this may look (marginally) more elegant, it will be significantly less efficient. The better approach is along the lines you were trying already.
Below I use the with clause (and give column names in the subquery declaration - that is only supported since Oracle 11.2, if your version is 11.1 you will need to move the aliases into the select clauses as you were doing in your attempt).
with
table_name (child, parent) as (
select 'abc', null from dual union all
select 'mno', 'abc' from dual union all
select 'def', 'abc' from dual union all
select '123', 'abc' from dual union all
select 'qrs', '123' from dual union all
select '789', 'def' from dual union all
select 'xyz', '123' from dual
)
, a (ancestor_path) as (
select sys_connect_by_path(child, '>')
from table_name
where connect_by_isleaf = 1
start with child = :i_child
connect by child = prior parent
)
, d (descendant_path) as (
select substr(sys_connect_by_path(child, '>'), 2)
from table_name
where connect_by_isleaf = 1
start with parent = :i_child
connect by parent = prior child
)
select d.descendant_path || a.ancestor_path as full_path
from d cross join a
;

Here's a method that first descends down the tree to get the root parent(s) from the chosen one.
Then uses those roots to climb back up.
The resulting paths are then filtered on the chosen child.
And what remains are those where the final child isn't a parent.
create table test_hierarchy (
child varchar(3),
parent varchar(3),
primary key (child)
);
insert into test_hierarchy (child, parent)
select 'abc' as child, null as parent from dual union all
select 'mno' as child, 'abc' as parent from dual union all
select 'def' as child, 'abc' as parent from dual union all
select '123' as child, 'abc' as parent from dual union all
select 'qrs' as child, '123' as parent from dual union all
select '789' as child, 'def' as parent from dual union all
select 'xyz' as child, '123' as parent from dual;
with cte (base) as (
select '123' from dual
)
select *
from
(
select
sys_connect_by_path(child,'>') as path
, cte.base
, level
, connect_by_root child as child
from test_hierarchy
cross join cte
where child in (
select connect_by_root child
from test_hierarchy
where child in (select base from cte)
and connect_by_root parent is null
connect by prior child = parent
)
connect by prior parent = child
) q
where path like '%'||base||'%'
and not exists (
select 1
from test_hierarchy t
where t.parent = q.child
)
PATH
BASE
LEVEL
CHILD
>qrs>123>abc
123
3
qrs
>xyz>123>abc
123
3
xyz
Demo on db<>fiddle here

Related

A view to get children when parent is given

I have a table with data of parent and child.
Parent | Child
---------------
A B
B C
C D
I want to get all the children form this table when I put where condition for parent.
ex -
select * from view where parent = 'A'
result should be B,C,D.
How do I write a view to get this output.
Thanks in advance.
Here's one option:
SQL> with test (parent, child) as
2 (select 'A', 'B' from dual union all
3 select 'B', 'C' from dual union all
4 select 'C', 'D' from dual
5 )
6 select max(ltrim(sys_connect_by_path(child, '-'), '-')) result
7 from test
8 connect by prior child = parent
9 start with parent = 'A';
RESULT
---------------------------------------------------------------------------
B-C-D
SQL>
You can do it using Listagg and Hierarchy clause
SQLFiddle
SELECT LISTAGG(CHILD, ',') WITHIN GROUP (ORDER BY CHILD) FROM (
SELECT CHILD FROM TABLE1 START WITH PARENT = 'A' CONNECT BY PRIOR CHILD = PARENT);

oracle hierarchy that gets wrong reference parent

I have the below data example for some items, those items have reference and reference parents. My issue is some references have wrong parents,which lead to infinite loop
let me explain, item with reference K001 doesn't have reference because it is the main product, reference k002 its parent is k001. k003 its parents is k001, data here is correct, because k001 is the parent with reference First.
For type B data is incorrect, m992 its parent is m993, however reference m993 its parent is m992, this is incorrect, the reference should point to a reference_parent First so to fix this the reference_parent for reference m992 should be m991 not m993.
For type C data also is incorrect, reference K883 has wrong reference_Parent because it hask886.
Because when I go to reference K886 I check its reference parent which is k885 then i check k885 reference parent which is k884 , then i check reference parent for k884 which is k883, then when I check k883 reference parent it gives me k886, here its wrong and I go an infinite loop, because k886 reference parent is kk85. To correct the reference parent of item k883 should be k882
Type Reference Reference_Parent
A K001 First
A K002 K001
A K003 K001
B M991 First
B M992 M993 --wrong parent data
B M993 M992
C K881 First
C K882 K881
C K883 K886 --wrong parent data
C K884 K883
C K885 K884
C K886 K885
A L001 First
A L002 L001
A L003 L002
A L004 L002
A L005 L002
A L006 L004
I tried with below query but its not giving me my required result and I am not expert with heirachy queries
SELECT reference,reference_parent,LEVEL ,SYS_CONNECT_BY_PATH(reference, '/') "Path"
FROM ITEMS
WHERE TYPE='B'
START WITH reference_parent = 'First'
CONNECT BY nocycle PRIOR reference_parent = 'First'
My required result is to give me
B M992 M993 --wrong parent data
C K883 K886 --wrong parent data
Oracle Setup:
CREATE TABLE ITEMS ( Type, Reference, Reference_Parent ) AS
SELECT 'A', 'K001', 'First' FROM DUAL UNION ALL
SELECT 'A', 'K002', 'K001' FROM DUAL UNION ALL
SELECT 'A', 'K003', 'K001' FROM DUAL UNION ALL
SELECT 'B', 'M991', 'First' FROM DUAL UNION ALL
SELECT 'B', 'M992', 'M993' FROM DUAL UNION ALL --wrong parent data
SELECT 'B', 'M993', 'M992' FROM DUAL UNION ALL
SELECT 'C', 'K881', 'First' FROM DUAL UNION ALL
SELECT 'C', 'K882', 'K881' FROM DUAL UNION ALL
SELECT 'C', 'K883', 'K886' FROM DUAL UNION ALL --wrong parent data
SELECT 'C', 'K884', 'K883' FROM DUAL UNION ALL
SELECT 'C', 'K885', 'K884' FROM DUAL UNION ALL
SELECT 'C', 'K886', 'K885' FROM DUAL UNION ALL
SELECT 'A', 'L001', 'First' FROM DUAL UNION ALL
SELECT 'A', 'L002', 'L001' FROM DUAL UNION ALL
SELECT 'A', 'L003', 'L002' FROM DUAL UNION ALL
SELECT 'A', 'L004', 'L002' FROM DUAL UNION ALL
SELECT 'A', 'L005', 'L002' FROM DUAL UNION ALL
SELECT 'A', 'L006', 'L004' FROM DUAL;
Query:
SELECT DISTINCT
MIN( CONNECT_BY_ROOT( Reference_Parent ) )
KEEP ( DENSE_RANK FIRST ORDER BY CONNECT_BY_ROOT( Reference ) )
AS reference_parent,
MIN( CONNECT_BY_ROOT( Reference ) )
AS reference
FROM ITEMS
START WITH Reference NOT IN (
SELECT reference
FROM ITEMS
START WITH Reference_Parent = 'First'
CONNECT BY PRIOR Reference = Reference_Parent
)
CONNECT BY NOCYCLE PRIOR Reference = Reference_Parent
GROUP BY Reference
Output:
REFERENCE_PARENT | REFERENCE
:--------------- | :--------
K886 | K883
M993 | M992
db<>fiddle here
Explanation:
You can find all the rows in the hierarchy starting with 'First' using the query:
SELECT reference
FROM ITEMS
START WITH Reference_Parent = 'First'
CONNECT BY PRIOR Reference = Reference_Parent
Your rows will be disconnected from these so you want a hierarchical query like:
SELECT *
FROM ITEMS
START WITH Reference NOT IN (
SELECT reference
FROM ITEMS
START WITH Reference_Parent = 'First'
CONNECT BY PRIOR Reference = Reference_Parent
)
CONNECT BY NOCYCLE PRIOR Reference = Reference_Parent
This just gives you the disconnected components and will generate lots of duplicate rows as it will start the hierarchy at every point in the cycle so some additional work is required to find the lowest Reference in the cycle (which appears to be where you are saying the wrong parents have occurred). This can be found by getting the MINimum values for CONNECT_BY_ROOT( Reference ) for each Reference; which gives you the query above.
Query 2:
You could also just look for cycles without eliminating the rows connected to 'First':
SELECT DISTINCT
MIN( Type ) KEEP ( DENSE_RANK FIRST ORDER BY Reference )
AS Type,
MIN( Reference ) AS Reference,
MIN( Reference_Parent ) KEEP ( DENSE_RANK FIRST ORDER BY Reference )
AS Reference_Parent
FROM (
SELECT Type,
Reference,
Reference_parent,
CONNECT_BY_ROOT( Reference_Parent ) AS root_reference_parent,
CONNECT_BY_ROOT( Reference ) AS root_reference,
CONNECT_BY_ISCYCLE AS isCycle
FROM ITEMS
CONNECT BY NOCYCLE PRIOR Reference = Reference_Parent
)
GROUP BY
root_Reference,
root_Reference_Parent
HAVING MAX( isCycle ) = 1
db<>fiddle here

Integrate a tree in oracle

I have a query like below:
select * from (
select * from (
select distinct * from TBL_IDPS_TREE
START WITH LEDGER_CODE in (
10912520000
,10825060000
,10912380000
,11311110201
)
CONNECT BY PRIOR parent_CODE = LEDGER_CODE
)a left join (
select /*+ PARALLEL(AUTO) */ balance as "y300" , ledger_code as "id",'' as "x300" , round(abs(balance)/30835,2) as "z300",name as "name" from tbl_ledger_archive where ledger_code in (
10912520000
,10825060000
,10912380000
,11311110201) and eff_date ='29-MAY-19'
) b
on a.LEDGER_CODE = b."id")
START WITH PARENT_CODE is null
connect by PRIOR LEDGER_CODE = Parent_CODE
;
and the result is :
x300,y300,z300 are the value of tree.
I want to change a query that integrate the tree value for x300,y300,z300
I mean the query must integrate tree value from leaves to root.
Use connect_by_root in the account tree subquery and join with ledger by it.
Demo:
with TBL_IDPS_TREE as (
select 10912520000 LEDGER_CODE, 1091252 parent_CODE from dual union all
select 10825060000, 1091252 from dual union all
select 1091252, 1091 from dual union all
select 1091, null from dual
), tbl_ledger_archive as (
select 500000 as "y300" , 10912520000 as "id", '' as "x300" , round(500000/30835, 2) as "z300", 'abc' as "name" from dual union all
select 600000 as "y300" , 10825060000 as "id", '' as "x300" , round(600000/30835, 2) as "z300", 'abc' as "name" from dual
)
select a.LEDGER_CODE, a.parent_CODE, l."x300", sum(l."y300") "y300", sum(l."z300") "z300"
from (
select distinct t.*, connect_by_root LEDGER_CODE as accRoot
from TBL_IDPS_TREE t
START WITH LEDGER_CODE in (
10912520000
,10825060000
,10912380000
,11311110201
)
CONNECT BY PRIOR parent_CODE = LEDGER_CODE
) a
left join tbl_ledger_archive l
on l."id" = a.accRoot
group by a.LEDGER_CODE, a.parent_CODE, l."x300" ;

How do I get top element of a certain type?

Let's say I have something similar to this (hierarchy of folders on drive C:).
I want to get top folder of a given folder (in this case I chose '1'), not the disc itself, how do I do this?
Hierarchy can have various levels.
with data as
(
select '1' name, 'folder' type, 'docs' parent from dual union
select '2' name, 'folder' type, 'docs' parent from dual union
select '3' name, 'folder' type, 'docs' parent from dual union
select 'docs' name, 'folder' type, 'MyFolder' parent from dual union
select 'MyFolder' name, 'folder' type, 'C:\' parent from dual union
select 'C:\' name, 'Drive' type, null parent from dual
)
select name, level from data
start with name = '1'
connect by prior parent = name
order by level;
expected output: 'Myfolder', as it doesn't have any other folder as parent.
Here's one option:
SQL> with data as
2 (
3 select '1' name , 'folder' type, 'docs' parent from dual union
4 select '2' name , 'folder' type, 'docs' parent from dual union
5 select '3' name , 'folder' type, 'docs' parent from dual union
6 select 'docs' name , 'folder' type, 'MyFolder' parent from dual union
7 select 'MyFolder' name, 'folder' type, 'C:\' parent from dual union
8 select 'C:\' name , 'Drive' type, null parent from dual
9 ),
10 inter as
11 (select name, level lvl, type
12 from data
13 start with name = '1'
14 connect by prior parent = name
15 )
16 select name
17 from inter
18 where lvl = (select max(lvl) from inter
19 where type = 'folder');
NAME
--------
MyFolder
SQL>
Please try the below,
WITH data
AS (SELECT '1' name, 'folder' TYPE, 'docs' parent FROM DUAL
UNION
SELECT '2' name, 'folder' TYPE, 'docs' parent FROM DUAL
UNION
SELECT '3' name, 'folder' TYPE, 'docs' parent FROM DUAL
UNION
SELECT 'docs' name, 'folder' TYPE, 'MyFolder' parent FROM DUAL
UNION
SELECT 'MyFolder' name, 'folder' TYPE, 'C:\' parent FROM DUAL
UNION
SELECT 'C:\' name, 'Drive' TYPE, NULL parent FROM DUAL)
SELECT name
FROM data
WHERE parent = (SELECT name
FROM data
WHERE TYPE = 'Drive')
START WITH name = '1'
CONNECT BY PRIOR parent = name
Output
NAME
--------
MyFolder
Test with the real data and let us know the outcome.

Find the top-level parent group each record belongs to

I have hierarchical data with the following structure. The levels are 1 to 6, so instead of doing 6 joins and then coalesce, how can I find what is the top-level parent (which has no parent_nr).
I've tried the accepted answer here,
SELECT
aa.code_nr,
aa.parent_nr,
CONNECT_BY_ROOT aa.code_nr AS "Top Level ID"
FROM mytable_t aa
CONNECT BY PRIOR aa.code_nr = aa.parent_nr
;
but it only gives me the next level as "Top Level ID" and not the final level (A)
Oracle Setup:
CREATE TABLE my_table ( code_nr, parent_nr ) AS (
SELECT 'A', NULL FROM DUAL UNION ALL
SELECT 'A.1', 'A' FROM DUAL UNION ALL
SELECT 'A.1.1', 'A.1' FROM DUAL UNION ALL
SELECT 'A.1.1.1', 'A.1.1' FROM DUAL UNION ALL
SELECT 'A.1.1.2', 'A.1.1' FROM DUAL UNION ALL
SELECT 'A.1.1.1.1', 'A.1.1.1' FROM DUAL UNION ALL
SELECT 'A.1.1.2.1', 'A.1.1.2' FROM DUAL UNION ALL
SELECT 'A.1.1.2.2', 'A.1.1.2' FROM DUAL;
Query:
SELECT LEVEL,
code_nr AS root_code_nr,
CONNECT_BY_ROOT( code_nr ) AS code_nr
FROM my_table
WHERE CONNECT_BY_ISLEAF = 1
CONNECT BY PRIOR parent_nr = code_nr;
Output:
LEVEL ROOT_CODE CODE_NR
----- --------- ---------
1 A A
2 A A.1
3 A A.1.1
4 A A.1.1.1
5 A A.1.1.1.1
4 A A.1.1.2
5 A A.1.1.2.1
5 A A.1.1.2.2
You could try the following:
SELECT
aa.code_nr,
aa.parent_nr,
substr(SYS_CONNECT_BY_PATH(aa.code_nr, '/'),2,instr(SYS_CONNECT_BY_PATH(aa.code_nr, '/'),'/'))
FROM mytable_t aa
CONNECT BY PRIOR aa.code_nr = aa.parent_nr
;