Get data from self-referencing table in all directions - sql

I have a table with such rows:
ID Parent_ID Name
1 (null) A
2 1 B
3 1 C
4 2 D
5 3 E
6 5 F
7 (null) G
8 (null) H
I need to get IDs of all related rows no matter if Name='A' or 'F' is passed as criteria. In this case I should receive all ID beside 7 and 8.
I tried lot of examples and read a lot of articles but I give up now. Can you help with it?

with
t as (
select id
from your_table
where name = 'D' -- your starting point
)
select id
from (
select id, parent_id from your_table
where parent_id is not null
union all
select parent_id, id from your_table
where parent_id is not null
union all
select id, null from t
)
start with parent_id is null
connect by nocycle prior id = parent_id
fiddle

A is at the root of a hierarchy (it's the parent of B, which is the parent of D, etc.). To start with A and work down to F (and also down to D and E, which also have A as a parent through different routes):
SELECT ID, Parent_ID, Name
FROM tbl
START WITH Name = 'A'
CONNECT BY PRIOR ID = Parent_ID
F is at the end of a hierarchy. Oracle calls this a "leaf". To start with leaf F and work up to A at the top:
SELECT ID, Parent_ID, Name
FROM tbl
START WITH Name = 'F' -- start with F instead of A
CONNECT BY PRIOR Parent_ID = ID -- switch the CONNECT BY to work up
Oracle has a SYS_CONNECT_BY_PATH function that's great for visualizing the hierarchy. Here's how to use it in the first query (A down to F):
SELECT ID, Parent_ID, Name, SYS_CONNECT_BY_PATH(Name, '/') AS Path
FROM tbl
START WITH Name = 'A'
CONNECT BY PRIOR ID = Parent_ID
Results:
ID PARENT_ID NAME PATH
---- ---------- ---- -----------
1 A /A
2 1 B /A/B
4 2 D /A/B/D
3 1 C /A/C
5 3 E /A/C/E
6 5 F /A/C/E/F
You can use any delimeter you want as the second argument to SYS_CONNECT_BY_PATH.

Related

Recursive CTE on every row in a stored procedure with parameter

I have a table called AccountNode.
Where the the column nodeId has a parent in the ParentNodeId col.
AccountNode
nodeId | ParentNodeId | Flag | SetId |
1 2 N 1
2 115 N 1
115 4 N 1
4 5 Y 1
12 13 N 1
13 14 N 1
14 15 Y 1
23 24 N 1
25 30 Y 1
What i need :
i need to get the parent node of each node id where the flag is Y (this is where we need to stop our recursive cte), for the setId passed as parameter to the procedure.
eg :
input:
for set_id : 1
output:
nodeId parentNode flag set_id
1 5 Y 1
12 15 Y 1
25 30 Y 1
i have written a recursive cte to get the parent node for a node id , but i am having trouble wrting it for a setid where
, i need to loop through all the nodeIds in a setid , to get the parentNode.
Here is my sql :
with accountNode_cte (nodeId, parentNode, flag, set_id) as
(select nodeId , parentNode, flag, set_id) from accountNode where nodeId = '1'
union all
select accountNode.nodeId, accountNode.parentNode, accountNode.flag, accountNode.set_id from
accountNode
join accountNode_cte on accountNode.nodeId = accountNode_cte.parentNode
and accountNode_cte.flag !='Y')
select * from accountNode_cte where flag='Y'
i am pretty new to writing sql procedures , not sure how to go about this
Traverse the node hierarchy from the starting nodes, remember starting point, track level, take max within starting point level rows.
with accountNode_cte(p, l, nodeId, parentNode, flag, set_id) as (
select parentNodeId p, 1 l, nodeId , parentNodeId, flag, set_id
from accountNode
where flag = 'Y' and set_id = 1
union all
select p, l+1, accountNode.nodeId, accountNode.parentNodeId, accountNode.flag, accountNode.set_id
from accountNode
join accountNode_cte on accountNode.parentnodeId = accountNode_cte.NodeId
)
select distinct first_value(nodeId) over(partition by p order by l desc) nodeId, p parentnId
from accountNode_cte;
db<>fiddle

DB2 : search depth first : (syntax) error

A few days ago I installed DB2 LUW (11.5) on a server to play around with.
Now I would like to do some recursive SQL (Recursive Common Table Expression):
Let me show how I setup :
drop table relations;
create table relations (id int, parent int);
insert into relations values(0,NULL);
insert into relations values(1,0);
insert into relations values(2,1);
insert into relations values(3,1);
insert into relations values(4,3);
insert into relations values(5,0);
insert into relations values(6,5);
insert into relations values(7,5);
insert into relations values(8,6);
insert into relations values(9,7);
insert into relations values(10,0);
insert into relations values(11,1);
commit;
Now I would like to see the hierarchy in the table. So I tried the following:
with recur(id, parent, level) as
(
select rel.id id, rel.parent parent, 0 level from relations rel where rel.id=0
union all
select rel.id, rel.parent, rec.level+1 from recur rec, relations rel where rec.id=rel.parent
and rec.level<10
)
select id, lpad(parent, level*2, ' ') from recur;
This gives me:
ID PARENT
----------- ------------------
0 -
1 0
5 0
10 0
2 1
3 1
11 1
6 5
7 5
4 3
8 6
9 7
This is (to me) : "Search Breadth First"
What I would like to see is "Search Depth First"
So I did this:
with recur(id, parent, level) as
(
select rel.id id, rel.parent parent, 0 level from relations rel where rel.id=0
union all
select rel.id, rel.parent, rec.level+1 from recur rec, relations rel where rec.id=rel.parent
and rec.level<10
)
search depth first by parent set ord
select id, lpad(parent, level*2, ' ') parent from recur order by ord;
But this delivers to me:
SQL0104N An unexpected token "search depth first by parent set ord sel" was
found following "t and rec.level<10 )". Expected tokens may include:
"<values>". SQLSTATE=42601
No clue how to solve it now. I (think I) have tried a lot of possible solutions. But none worked.
I'm starting to believe that DB2 LUW (11.5) doesn't know about Search Depth First. Or some setting must be made to make DB2 aware of the "SDF" possibility.
My question to you all:
How to solve this problem? How do I get Search Depth First to work?
On the positive....following works like a charms....but that is not whatI want tot know :-)
select id, lpad(parent, level*2, ' ') parent, level
from relations
start with id=0
connect by prior id=parent;
ID PARENT LEVEL
----------- ---------- -----------
0 - 1
1 0 2
2 1 3
3 1 3
4 3 4
11 1 3
5 0 2
6 5 3
8 6 4
7 5 3
9 7 4
10 0 2
This works like a charm, but I had tot make a switch in the database (and a restart):
db2set DB2_COMPATIBILITY_VECTOR=08
Your question is about displaying rows in a specific ordering, not about searching in a specific ordering.
You can display the rows in the ordering you want by assembling an ordering column that fits your needs.
For example:
with
n (id, parent, lvl, ordering) as (
select id, parent, 1, lpad(id, 3, '0') || lpad('', 30, ' ')
from relations
where parent is null
union all
select r.id, r.parent, n.lvl + 1, trim(n.ordering) || '/' || lpad(r.id, 3, '0')
from n, relations r where r.parent = n.id
)
select id, lpad(parent, lvl * 2, ' ') as parent, lvl
from n
order by ordering;
Result:
ID PARENT LVL
--- --------- ---
0 1
1 0 2
2 1 3
3 1 3
4 3 4
11 1 3
5 0 2
6 5 3
8 6 4
7 5 3
9 7 4
10 0 2
See running example at db<>fiddle.

Select unique subsets

I have a table like in example below.
SQL> select * from test;
ID PARENT_ID NAME
1 1 A
2 1 B
3 2 A
4 2 B
5 3 A
6 3 B
7 3 C
8 4 A
What I need is to get all unique subsets of names ((A,B), (A,B,C), (A)) or exclude duplicate subsets. You can see that (A,B) is twice there, one for PARENT_ID=1 and one for 2.
I want to exclude such duplicates:
ID PARENT_ID NAME
1 1 A
2 1 B
5 3 A
6 3 B
7 3 C
8 4 A
You can use DISTINCT to only return different values.
e.g.
SELECT DISTINCT GROUP_CONCAT(NAME SEPARATOR ',') as subsets
FROM TABLE_1
GROUP BY PARENT_ID;
SQL Fiddle
I have used 'group_concat' assuming you are using 'Mysql'. The equivalent function in Oracle is 'listagg()'. you can see it in action here in SQL fiddle
Here is the solution:-
Select a.* from
test a
inner join
(
Select nm, min(parent_id) as p_id
from
(
Select Parent_id, group_concat(NAME) as nm
from test
group by Parent_ID
) a
group by nm
)b
on a.Parent_id=b.p_id
order by parent_id, name

SQL: Hierarchical query with multiple roots / parents

I have a table describing elements organized in a tree-like structure:
ID, PARENT_ID, NAME
0 null TOP
1 0 A
2 0 B
3 0 C
4 1 AA
5 2 BA
6 3 CA
7 6 CAA
...
There can be many levels in this hierarchy.
Suppose there is a list of elements (say IDs 2 and 3) for which I would like to get all child records from the table.
Something like this:
select *
from MY_TABLE
start with PARENT_ID in (2,3)
connect by PARENT_ID = prior ID
will return:
ID, PARENT_ID, NAME
5 2 BA
6 3 CA
7 6 CAA
However, I want the each output record to be mapped to the original parent from my list (2,3) so that the output would look like this:
ORIGINAL_PARENT_ID, ID, PARENT_ID, NAME
2 5 2 BA
3 6 3 CA
3 7 6 CAA
How can it be done?
connect_by_root may be what you're after?
SQL> select t.*, connect_by_root parent_id as ORIGINAL_PARENT_ID
2 from MY_TABLE t
3 start with PARENT_ID in (2,3)
4 connect by PARENT_ID = prior ID
5 /
ID PARENT_ID NAM ORIGINAL_PARENT_ID
---------- ---------- --- ------------------
5 2 BA 2
6 3 CA 3
7 6 CAA 3
Assuming your names are really as you have them, then the problem can be done without connect by. You can use simple string manipulation.
with ToFind (
select 'C' as parent from dual union all
select 'B' as parent from dual
)
select t.*
from t join
ToFind tf
on t.name like tf.parent, 100)||'%' and t.name <> tf.parent

Oracle analytic function - using FIRST_VALUE to remove unwanted rows

I believe the Oracle function FIRST_VALUE is what I need to be using based on these two questions:
SQL - How to select a row having a column with max value
Oracle: Taking the record with the max date
I have 3 tables that represent people associated with organizations. Each organization may have a parent org, where ORG.PARENT is a foreign key to ORG.ID (so the table refers to itself). A person may be associated with more than one group.
PERSON
ID NAME
----------
1 Bob
ORG
ID NAME PARENT
------------------------
1 A (null)
2 A-1 1
3 A-2 1
4 A-3 1
5 A-1-a 2
6 A-1-b 2
7 A-2-a 3
8 A-2-b 3
PERSON_TO_ORG
PERSON_ID ORG_ID
-----------------
1 1
1 3
I want to list the groups a person is associated with so I used this query:
SELECT NAME, ID, sys_connect_by_path(NAME, '/') AS path
FROM org
START WITH ID IN
(SELECT org_id FROM person_to_org WHERE person_id=1)
connect by prior org.ID = org.parent;
...which gives me:
NAME ID PATH
------------------
A-2 3 /A-2
A-2-a 8 /A-2/A-2-a
A-2-b 9 /A-2/A-2-b
A 1 /A
A-1 2 /A/A-1
A-1-a 5 /A/A-1/A-1-a
A-1-b 6 /A/A-1/A-1-b
A-2 3 /A/A-2
A-2-a 8 /A/A-2/A-2-a
A-2-b 9 /A/A-2/A-2-b
A-3 4 /A/A-3
Notice how A-2 appears twice, as it should. I don't want a group to appear twice, however. I want a group to only appear at its lowest level in the tree, i.e. at its highest level value. Here is how I've tried using FIRST_VALUE with no luck - I still get A-2 (and others) appearing twice:
SELECT id, name, path, first_value(lev) OVER
(
PARTITION BY ID,NAME, path ORDER BY lev DESC
) AS max_lev FROM
(SELECT NAME, ID, sys_connect_by_path(NAME, '/') AS path, LEVEL as lev
FROM org START WITH ID IN
(SELECT org_id FROM person_to_org WHERE person_id=1)
connect by prior org.ID = org.parent);
This seems similar to the FIRST_VALUE example in Pro Oracle SQL but I can't seem to make it work no matter how I tweak the parameters.
How can I return only the rows where a given group has its highest level value (i.e. farthest down in the tree)?
As also said in one of the threads you refer to, analytics are not the most efficient way to go here: you need to aggregate to filter out the duplicates.
SQL> SELECT id
2 , max(name) keep (dense_rank last order by lev) name
3 , max(path) keep (dense_rank last order by lev) path
4 FROM ( SELECT NAME
5 , ID
6 , sys_connect_by_path(NAME, '/') AS path
7 , LEVEL as lev
8 FROM org
9 START WITH ID IN (SELECT org_id FROM person_to_org WHERE person_id=1)
10 connect by prior org.ID = org.parent
11 )
12 group by id
13 /
ID NAME PATH
---------- ----- --------------------
1 A /A
2 A-1 /A/A-1
3 A-2 /A/A-2
4 A-3 /A/A-3
5 A-1-a /A/A-1/A-1-a
6 A-1-b /A/A-1/A-1-b
7 A-2-a /A/A-2/A-2-a
8 A-2-b /A/A-2/A-2-b
8 rows selected.
Regards,
Rob.
PS: Here is some more information about the LAST aggregate function: http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions071.htm#sthref1495
What about this (untested)
SELECT
SELECT id,
name,
path
FROM (
SELECT id,
name,
path,
row_number() over (partition by id,name order by lev desc) as rn
FROM (
SELECT NAME,
ID,
sys_connect_by_path(NAME, '/') AS path,
LEVEL as lev
FROM org
START WITH ID IN (SELECT org_id FROM person_to_org WHERE person_id=1)
connect by prior org.ID = org.parent
)
)
where rn = 1
You should partition only OVER (PARTITION BY ID,NAME ORDER BY lev DESC)
not ID,NAME, path
Edit:
And probably you want first_value(path), not first_value(lev)