recursive query to find the root parent in oracle - sql

I am trying to figure out the root parent in a table with hierarchical data. The following example works as expected but I need to do something extra. I want to avoid the query to ignore null id1 and show the (root parent - 1) if the root parent is null.
with table_a ( id1, child_id ) as (
select null, 1 from dual union all
select 1, 2 from dual union all
select 2, 3 from dual union all
select 3, NULL from dual union all
select 4, NULL from dual union all
select 5, 6 from dual union all
select 6, 7 from dual union all
select 7, 8 from dual union all
select 8, NULL from dual
)
select connect_by_root id1 as id, id1 as root_parent_id
from table_a
where connect_by_isleaf = 1
connect by child_id = prior id1
order by id 1
This brings up the following data
4 4
6 5
7 5
8 5
5 5
3 null
null null
2 null
1 null
what I want is
3 1
1 1
2 1
4 4
7 5
8 5
5 5
6 5
is it possible?
Thanks for the help

Using a recursive CTE you can do:
with table_a ( id1, child_id ) as (
select null, 1 from dual union all
select 1, 2 from dual union all
select 2, 3 from dual union all
select 3, NULL from dual union all
select 4, NULL from dual union all
select 5, 6 from dual union all
select 6, 7 from dual union all
select 7, 8 from dual union all
select 8, NULL from dual
),
n (s, e) as (
select id1 as s, child_id as e from table_a where id1 not in
(select child_id from table_a
where id1 is not null and child_id is not null)
union all
select n.s, a.child_id
from n
join table_a a on a.id1 = n.e
)
select
coalesce(e, s) as c, s
from n
order by s
Result:
C S
- -
3 1
1 1
2 1
4 4
5 5
7 5
8 5
6 5
As a side note, "Recursive CTEs" are more flexible than the old-school CONNECT BY.

This looks like it works but it may be incorrect, as I do not quite understand the logic behind choosing 1 for this, looks arbitrary to me, not much like real data will be.
As Hogan has asked already, it would be helpful if you could perhaps provide an explanation or an expanded data set to test this hierarchy.
with table_a ( id1, child_id ) as (
select null, 1 from dual union all
select 1, 2 from dual union all
select 2, 3 from dual union all
select 3, NULL from dual union all
select 4, NULL from dual union all
select 5, 6 from dual union all
select 6, 7 from dual union all
select 7, 8 from dual union all
select 8, NULL from dual
)
select connect_by_root id1 as id, id1 as root_parent_id
from table_a
where connect_by_isleaf = 1 and connect_by_root id1 is not null
connect by nocycle child_id = prior nvl(id1, 1)
order by 2, 1;
Sample execution:
FSITJA#dbd01 2019-07-19 13:51:13> with table_a ( id1, child_id ) as (
2 select null, 1 from dual union all
3 select 1, 2 from dual union all
4 select 2, 3 from dual union all
5 select 3, NULL from dual union all
6 select 4, NULL from dual union all
7 select 5, 6 from dual union all
8 select 6, 7 from dual union all
9 select 7, 8 from dual union all
10 select 8, NULL from dual
11 )
12 select connect_by_root id1 as id, id1 as root_parent_id
13 from table_a
14 where connect_by_isleaf = 1 and connect_by_root id1 is not null
15 connect by nocycle child_id = prior nvl(id1, 1)
16 order by 2, 1;
ID ROOT_PARENT_ID
---------- --------------
1 1
2 1
3 1
4 4
5 5
6 5
7 5
8 5
8 rows selected.

Related

Having a node at level N

I want to write the code by Having a node at level N (for example 3) of the tree, find all members up to level M (for example 6).How can I write this code? what is the best idea for this problem?
I am new in oracle , please explain me and help me for resolve this problem .
CREATE TABLE tree (
node_id INT,
parent_id INT
)
INSERT INTO
tree
SELECT 1, NULL FROM DUAL
UNION ALL SELECT 2, 1 FROM DUAL
UNION ALL SELECT 3, 1 FROM DUAL
UNION ALL SELECT 4, 2 FROM DUAL
UNION ALL SELECT 5, 2 FROM DUAL
UNION ALL SELECT 6, 3 FROM DUAL
UNION ALL SELECT 7, 3 FROM DUAL
UNION ALL SELECT 8, 4 FROM DUAL
UNION ALL SELECT 9, 4 FROM DUAL
UNION ALL SELECT 10, 5 FROM DUAL
UNION ALL SELECT 11, 5 FROM DUAL
UNION ALL SELECT 12, 6 FROM DUAL
UNION ALL SELECT 13, 6 FROM DUAL
UNION ALL SELECT 14, 7 FROM DUAL
UNION ALL SELECT 15, 7 FROM DUAL
15 rows affected
SELECT
node_id, parent_id, level
FROM
tree
WHERE
level < 3
CONNECT BY
PRIOR node_id = parent_id
START WITH
parent_id = 3
ORDER BY
level, node_id
NODE_ID
PARENT_ID
LEVEL
6
3
1
7
3
1
12
6
2
13
6
2
14
7
2
15
7
2
fiddle

Cross SQL joins through multiple tables

I have data in multiple tables I need to cross join via different attributes to achieve the output. i.e.
ID
Node
1
A
2
B
3
C
4
D
5
E
6
G
ID
ParentID
1
100
2
200
3
300
4
400
5
500
6
600
7
100
8
200
9
300
10
700
11
800
12
800
ID
Splice Name
7
Irvine
8
Goodyear
9
Phoenix
10
Seattle
11
Augusta
12
Atlanta
Every table has a unique Child ID which corresponds to a Parent ID that is NOT unique. Multiple child IDs can be associated to the same Parent ID (as you can see above, child ID 1 and 7 for instance share Parent ID 100. The ID in each table corresponds with a unique Child ID not shared with any other tables.
What I want to do in my query is have the Nodes show what their Splice Name is. So I want to do select Node, Splice_Name. Expected output for instance would show Node A as having Splice Name Irvine. The example above is only 3 tables and a few rows but I'm working with big data (500 tables and over a million rows)
My question is, how do I write my query to do multiple cross joins? Also my example above is crossing over through one table to get data from another, but how would you cross through multiple tables?
You do not want to CROSS JOIN. Instead, you can use INNER JOINs:
SELECT n.node, s.splice_name
FROM (
relationships rn
INNER JOIN nodes n
ON (rn.id = n.id)
)
INNER JOIN
(
relationships rs
INNER JOIN splices s
ON (rs.id = s.id)
)
ON (rn.parentid = rs.parentid);
Note: The braces are not required here, I just find they add clarity to the precedence of the joins.
Which, for the sample data:
CREATE TABLE nodes (ID, Node) As
SELECT 1, 'A' FROM DUAL UNION ALL
SELECT 2, 'B' FROM DUAL UNION ALL
SELECT 3, 'C' FROM DUAL UNION ALL
SELECT 4, 'D' FROM DUAL UNION ALL
SELECT 5, 'E' FROM DUAL UNION ALL
SELECT 6, 'G' FROM DUAL;
CREATE TABLE relationships (ID, ParentID) AS
SELECT 1, 100 FROM DUAL UNION ALL
SELECT 2, 200 FROM DUAL UNION ALL
SELECT 3, 300 FROM DUAL UNION ALL
SELECT 4, 400 FROM DUAL UNION ALL
SELECT 5, 500 FROM DUAL UNION ALL
SELECT 6, 600 FROM DUAL UNION ALL
SELECT 7, 100 FROM DUAL UNION ALL
SELECT 8, 200 FROM DUAL UNION ALL
SELECT 9, 300 FROM DUAL UNION ALL
SELECT 10, 700 FROM DUAL UNION ALL
SELECT 11, 800 FROM DUAL UNION ALL
SELECT 12, 800 FROM DUAL;
CREATE TABLE splices (ID, Splice_Name) AS
SELECT 7, 'Irvine' FROM DUAL UNION ALL
SELECT 8, 'Goodyear' FROM DUAL UNION ALL
SELECT 9, 'Phoenix' FROM DUAL UNION ALL
SELECT 10, 'Seattle' FROM DUAL UNION ALL
SELECT 11, 'Augusta' FROM DUAL UNION ALL
SELECT 12, 'Atlanta' FROM DUAL;
Outputs:
NODE
SPLICE_NAME
A
Irvine
B
Goodyear
C
Phoenix
If you want all the nodes and splices, including those without a corresponding relationship then use a FULL OUTER JOIN:
SELECT n.node, s.splice_name
FROM (
relationships rn
INNER JOIN nodes n
ON (rn.id = n.id)
)
FULL OUTER JOIN
(
relationships rs
INNER JOIN splices s
ON (rs.id = s.id)
)
ON (rn.parentid = rs.parentid)
Which outputs:
NODE
SPLICE_NAME
A
Irvine
B
Goodyear
C
Phoenix
(null)
Seattle
(null)
Augusta
(null)
Atlanta
D
(null)
G
(null)
E
(null)
fiddle
Maybe you could consider the solution based on Left Joins.
WITH
nodes AS
(
Select 1 "ID", 'A' "NODE" From Dual Union All
Select 2 "ID", 'B' "NODE" From Dual Union All
Select 3 "ID", 'C' "NODE" From Dual Union All
Select 4 "ID", 'D' "NODE" From Dual Union All
Select 5 "ID", 'E' "NODE" From Dual Union All
Select 6 "ID", 'F' "NODE" From Dual
),
parents AS
(
Select 1 "ID", 100 "PARENT_ID" From Dual Union All
Select 2 "ID", 200 "PARENT_ID" From Dual Union All
Select 3 "ID", 300 "PARENT_ID" From Dual Union All
Select 4 "ID", 400 "PARENT_ID" From Dual Union All
Select 5 "ID", 500 "PARENT_ID" From Dual Union All
Select 6 "ID", 600 "PARENT_ID" From Dual Union All
Select 7 "ID", 100 "PARENT_ID" From Dual Union All
Select 8 "ID", 200 "PARENT_ID" From Dual Union All
Select 9 "ID", 300 "PARENT_ID" From Dual Union All
Select 10 "ID", 700 "PARENT_ID" From Dual Union All
Select 11 "ID", 800 "PARENT_ID" From Dual Union All
Select 12 "ID", 800 "PARENT_ID" From Dual
),
splices AS
(
Select 7 "ID", 'Irwine' "SPLICE_NAME" From Dual Union All
Select 8 "ID", 'Goodyear' "SPLICE_NAME" From Dual Union All
Select 9 "ID", 'Phoenix' "SPLICE_NAME" From Dual Union All
Select 10 "ID", 'Seattle' "SPLICE_NAME" From Dual Union All
Select 11 "ID", 'Augusta' "SPLICE_NAME" From Dual Union All
Select 12 "ID", 'Atlanta' "SPLICE_NAME" From Dual
)
Select
p.PARENT_ID,
n.ID "NODE_ID", n.NODE,
p.SPLICE_ID, p.ID "ID",
CASE WHEN p.SPLICE_ID Is Null Then p.SPLICE_NAME
ELSE (Select SPLICE_NAME From splices Where ID = p.SPLICE_ID)
END "SPLICE_NAME"
From
(
Select
p.PARENT_ID, p.ID "ID",
p.SPLICE_ID "SPLICE_ID", p.SPLICE_NAME "SPLICE_NM", Max(p.SPLICE_NAME) OVER(Partition By PARENT_ID) "SPLICE_NAME"
From
(
Select p1.PARENT_ID, p1.ID, s1.ID "SPLICE_ID", s1.SPLICE_NAME
From parents p1
Left Join splices s1 ON(s1.ID = p1.ID)
)p
) p
Left Join nodes n ON(n.ID = p.ID)
Order By p.ID
/*
PARENT_ID NODE_ID NODE SPLICE_ID ID SPLICE_NAME
---------- ---------- ---- ---------- ---------- -----------
100 1 A 1 Irwine
200 2 B 2 Goodyear
300 3 C 3 Phoenix
400 4 D 4
500 5 E 5
600 6 F 6
100 7 7 Irwine
200 8 8 Goodyear
300 9 9 Phoenix
700 10 10 Seattle
800 11 11 Augusta
800 12 12 Atlanta
*/
The resulting dataset could be seelected/filtered/grouped/ordered or whatever you want...
Additionaly:
if there is another table with node data like here:
nodes_2 AS
(
Select 7 "ID", 'G' "NODE" From Dual Union All
Select 8 "ID", 'H' "NODE" From Dual
),
... then just change last Left Join to union of nodes and nodes_2 (_3, _4, _n). The same could be done for splices (Left Join in inner query).. and the result would be:
PARENT_ID NODE_ID NODE SPLICE_ID ID SPLICE_NAME
---------- ---------- ---- ---------- ---------- -----------
100 1 A 1 Irwine
200 2 B 2 Goodyear
300 3 C 3 Phoenix
400 4 D 4
500 5 E 5
600 6 F 6
100 7 G 7 7 Irwine
200 8 H 8 8 Goodyear
300 9 9 Phoenix
700 10 10 Seattle
800 11 11 Augusta
800 12 12 Atlanta

Get parent id from level with Oracle SQL

I have a hierarchical structure defined by level and order of elements. Is it possible to create "parent_id" column with Oracle SQL without using procedures?
I need to generate red values:
test data:
with t as
(
select 1 id, 'element1' name, 1 level_ from dual union all
select 2 id, 'element2' name, 2 level_ from dual union all
select 3 id, 'element3' name, 3 level_ from dual union all
select 4 id, 'element4' name, 3 level_ from dual union all
select 5 id, 'element5' name, 3 level_ from dual union all
select 6 id, 'element6' name, 3 level_ from dual union all
select 7 id, 'element7' name, 2 level_ from dual union all
select 8 id, 'element8' name, 3 level_ from dual union all
select 9 id, 'element9' name, 4 level_ from dual union all
select 10 id, 'element10' name, 4 level_ from dual union all
select 11 id, 'element11' name, 1 level_ from dual union all
select 12 id, 'element12' name, 2 level_ from dual union all
select 13 id, 'element13' name, 3 level_ from dual union all
select 14 id, 'element14' name, 4 level_ from dual union all
select 15 id, 'element15' name, 4 level_ from dual union all
select 16 id, 'element16' name, 3 level_ from dual union all
select 17 id, 'element17' name, 4 level_ from dual union all
select 18 id, 'element18' name, 4 level_ from dual union all
select 19 id, 'element19' name, 1 level_ from dual
)
select * from t
From Oracle 12, you can use MATCH_RECOGNIZE:
select *
from t
MATCH_RECOGNIZE (
ORDER BY id DESC
MEASURES
child.id AS id,
child.name AS name,
child.lvl AS lvl,
parent.id AS parent_id
ONE ROW PER MATCH
AFTER MATCH SKIP TO NEXT ROW
PATTERN (child ancestors*? (parent | $))
DEFINE
parent AS lvl = child.lvl - 1
)
ORDER BY id
Or, again from Oracle 12, a LATERAL join:
select *
from t c
LEFT OUTER JOIN LATERAL(
SELECT p.id AS parent_id
FROM t p
WHERE c.id > p.id
AND c.lvl = p.lvl + 1
ORDER BY id DESC
FETCH FIRST ROW ONLY
)
ON (1 = 1)
ORDER BY id
Or, in earlier versions:
SELECT id, name, lvl, parent_id
FROM (
SELECT c.*,
p.id AS parent_id,
ROW_NUMBER() OVER (PARTITION BY c.id ORDER BY p.id DESC) AS rn
FROM t c
LEFT OUTER JOIN t p
ON (c.id > p.id AND c.lvl = p.lvl + 1)
)
WHERE rn = 1
ORDER BY id
Which, for the sample data:
CREATE TABLE t (id, name, lvl ) as
select 1, 'element1', 1 from dual union all
select 2, 'element2', 2 from dual union all
select 3, 'element3', 3 from dual union all
select 4, 'element4', 3 from dual union all
select 5, 'element5', 3 from dual union all
select 6, 'element6', 3 from dual union all
select 7, 'element7', 2 from dual union all
select 8, 'element8', 3 from dual union all
select 9, 'element9', 4 from dual union all
select 10, 'element10', 4 from dual union all
select 11, 'element11', 1 from dual union all
select 12, 'element12', 2 from dual union all
select 13, 'element13', 3 from dual union all
select 14, 'element14', 4 from dual union all
select 15, 'element15', 4 from dual union all
select 16, 'element16', 3 from dual union all
select 17, 'element17', 4 from dual union all
select 18, 'element18', 4 from dual union all
select 19, 'element19', 1 from dual;
All output:
ID
NAME
LVL
PARENT_ID
1
element1
1
null
2
element2
2
1
3
element3
3
2
4
element4
3
2
5
element5
3
2
6
element6
3
2
7
element7
2
1
8
element8
3
7
9
element9
4
8
10
element10
4
8
11
element11
1
null
12
element12
2
11
13
element13
3
12
14
element14
4
13
15
element15
4
13
16
element16
3
12
17
element17
4
16
18
element18
4
16
19
element19
1
null
db<>fiddle here

Find oldest ancestor for each entry in a SQL table

I have a SQL table where the rows in the table have a parent-child relationship with each other. I would like to write a recursive SQL query to find the oldest ancestor for each of these rows (or the row itself if it has no parent). So in other words, if my table looks like this:
Child
Parent
1
null
2
1
3
2
4
null
5
4
6
null
7
null
8
3
9
5
Then my final output should be:
Child
OldestAncestor
1
1
2
1
3
1
4
4
5
4
6
6
7
7
8
1
9
4
Can this be done? I know how to use recursive SQL to find an individual parent-child relationship, I'm just not sure if that can be taken a step further to find the parent's parent, etc.
Update - I was able to figure out a solution based on this article:https://www.webcodeexpert.com/2020/04/sql-server-cte-recursive-query-to-get.html
WITH parentChild(Child, Parent) AS
(
SELECT 1, null UNION ALL
SELECT 2, 1 UNION ALL
SELECT 3, 2 UNION ALL
SELECT 4, null UNION ALL
SELECT 5, 4 UNION ALL
SELECT 6, null UNION ALL
SELECT 7, null UNION ALL
SELECT 8, 3 UNION ALL
SELECT 9, 5
),
MyCTE AS
(
SELECT pc.Child, pc.Child AS 'OldestAncestor'
FROM parentChild pc
WHERE pc.Parent IS NULL
UNION ALL
SELECT pc2.Child, m.OldestAncestor AS 'OldestAncestor'
FROM parentChild pc2
INNER JOIN MyCTE m ON pc2.Parent = m.Child
)
SELECT * FROM MyCTE ORDER BY Child
In Oracle, you can use:
SELECT child,
CONNECT_BY_ROOT child AS oldest_ancestor
FROM table_name
START WITH parent IS NULL
CONNECT BY parent = PRIOR child
ORDER BY child;
or a recursive sub-query factoring clause (WITH clause):
WITH rsqfc (child, oldest_ancestor) AS (
SELECT child, child
FROM table_name
WHERE parent IS NULL
UNION ALL
SELECT t.child, r.oldest_ancestor
FROM rsqfc r
INNER JOIN table_name t
ON (t.parent = r.child)
)
SELECT *
FROM rsqfc
ORDER BY child;
Which, for the sample data:
CREATE TABLE table_name (Child, Parent) AS
SELECT 1, null FROM DUAL UNION ALL
SELECT 2, 1 FROM DUAL UNION ALL
SELECT 3, 2 FROM DUAL UNION ALL
SELECT 4, null FROM DUAL UNION ALL
SELECT 5, 4 FROM DUAL UNION ALL
SELECT 6, null FROM DUAL UNION ALL
SELECT 7, null FROM DUAL UNION ALL
SELECT 8, 3 FROM DUAL UNION ALL
SELECT 9, 5 FROM DUAL;
Both output:
CHILD
OLDEST_ANCESTOR
1
1
2
1
3
1
4
4
5
4
6
6
7
7
8
1
9
4
db<>fiddle here
You can use a recursive CTE to find the paths from the parents to the leaf nodes, and then join the CTE back onto the original table:
with recursive cte(n1, n2) as (
select child, child from tbl where parent is null
union all
select c.n1, t.child from cte c join tbl t on c.n2 = t.parent
)
select t1.child, c.n1 from tbl t1 join cte c on t1.child = c.n2;

Oracle SQL: Find root parent for hierarchical data with child_id (No parent_id)

Say I have table A:
id child_id
1 2
2 3
3 NULL
4 NULL
5 6
6 7
7 8
8 NULL
I need a query to get root parent id of each row, this output for example:
id root_parent_id
1 1
2 1
3 1
4 4
5 5
6 5
7 5
8 5
I've tried both CONNECT BY and CTE by examples, but seems all I found based on rows with parent_id and not child_id and doesn't work.
CONNECT BY query I tried:
SELECT id, child_id, LEVEL, connect_by_root id
FROM a
CONNECT BY id = PRIOR child_id
CTE query I tried:
WITH recursion_view (base,
id,
child_id)
AS (
SELECT id base, id, child_id FROM a
UNION ALL
SELECT
previous_level.base,
current_level.id,
current_level.child_id
FROM recursion_view previous_level, a current_level
WHERE current_level.id = previous_level.child_id)
SELECT base,
id,
child_id
FROM recursion_view
ORDER BY base, id, child_id
You need to traverse the hierarchy in reverse order - view the root parents as leaves. Something like this:
with table_a ( id, child_id ) as (
select 1, 2 from dual union all
select 2, 3 from dual union all
select 3, NULL from dual union all
select 4, NULL from dual union all
select 5, 6 from dual union all
select 6, 7 from dual union all
select 7, 8 from dual union all
select 8, NULL from dual
)
select connect_by_root id as id, id as root_parent_id
from table_a
where connect_by_isleaf = 1
connect by child_id = prior id
order by id
;
ID ROOT_PARENT_ID
-- --------------
1 1
2 1
3 1
4 4
5 5
6 5
7 5
8 5