SQL: Hierarchical query with multiple roots / parents - sql

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

Related

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.

Get all of child nodes in a hierarchical data in SQL

I have the following sql table:
id parent_id
1 null
2 1
3 4
4 8
5 1
6 2
7 6
8 null
How can i get all of child nodes of some specific node?
For example with id = 1:
1 2 5 6 7
with id = 8
8 4 2
I've figured out in another blog, hope it will help other people:
with RECURSIVE cte(id,parent_id) as (
select
id,
parent_id
from forum
where id = 2
UNION ALL
select
forum.id,
forum.parent_id
from forum
inner join cte on forum.parent_id = cte.id
)
select * from cte

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: Assembling Non-Overlapping Sets

I have sets of consecutive integers, organized by type, in table1. All values are between 1 and 10, inclusive.
table1:
row_id set_id type min_value max_value
1 1 a 1 3
2 2 a 4 10
3 3 a 6 10
4 4 a 2 5
5 5 b 1 9
6 6 c 1 7
7 7 c 3 10
8 8 d 1 2
9 9 d 3 3
10 10 d 4 5
11 11 d 7 10
In table2, within each type, I want to assemble all possible maximal, non-overlapping sets (though gaps that cannot be filled by any sets of the correct type are okay). Desired output:
table2:
row_id type group_id set_id
1 a 1 1
2 a 1 2
3 a 2 1
4 a 2 3
5 a 3 3
6 a 3 4
7 b 4 5
8 c 5 6
9 c 6 7
10 d 7 8
11 d 7 9
12 d 7 10
13 d 7 11
My current idea is to use the fact that there is a limited number of possible values. Steps:
Find all sets in table1 containing value 1. Copy them into table2.
Find all sets in table1 containing value 2 and not already in table2.
Join the sets from (2) with table1 on type, set_id, and having min_value greater than the group's greatest max_value.
For the sets from (2) that did not join in (3), insert them into table2. These start new groups that may be extended later.
Repeat steps (2) through (4) for values 3 through 10.
I think this will work, but it has a lot of pain-in-the-butt steps, especially for (2)--finding the sets not in table2, and (4)--finding the sets that did not join.
Do you know a faster, more efficient method? My real data has millions of sets, thousands of types, and hundreds of values (though fortunately, as in the example, the values are bounded), so scalability is essential.
I'm using PLSQL Developer with Oracle 10g (not 11g as I stated before--thanks, IT department). Thanks!
For Oracle 10g you can't use recursive CTEs, but with a bit of work you can do something similar with the connect by syntax. First you need to generate a CTE or in-line view which has all the non-overlapping links, which you can do with:
select t1.type, t1.set_id, t1.min_value, t1.max_value,
t2.set_id as next_set_id, t2.min_value as next_min_value,
t2.max_value as next_max_value,
row_number() over (order by t1.type, t1.set_id, t2.set_id) as group_id
from table1 t1
left join table1 t2 on t2.type = t1.type
and t2.min_value > t1.max_value
where not exists (
select 1
from table1 t4
where t4.type = t1.type
and t4.min_value > t1.max_value
and t4.max_value < t2.min_value
)
order by t1.type, group_id, t1.set_id, t2.set_id;
This took a bit of experimentation and it's certainly possible I've missed or lost something about the rules in the process; but that gives you 12 pseudo-rows, and is in my previous answer this allows the two separate chains starting with a/1 to be followed while constraining the d values to a single chain:
TYPE SET_ID MIN_VALUE MAX_VALUE NEXT_SET_ID NEXT_MIN_VALUE NEXT_MAX_VALUE GROUP_ID
---- ------ ---------- ---------- ----------- -------------- -------------- --------
a 1 1 3 2 4 10 1
a 1 1 3 3 6 10 2
a 2 4 10 3
a 3 6 10 4
a 4 2 5 3 6 10 5
b 5 1 9 6
c 6 1 7 7
c 7 3 10 8
d 8 1 2 9 3 3 9
d 9 3 3 10 4 5 10
d 10 4 5 11 7 10 11
d 11 7 10 12
And that can be used as a CTE; querying that with a connect-by loop:
with t as (
... -- same as above query
)
select t1.type,
dense_rank() over (partition by null
order by connect_by_root group_id) as group_id,
t1.set_id
from t t1
connect by type = prior type
and set_id = prior next_set_id
start with not exists (
select 1 from table1 t2
where t2.type = t1.type
and t2.max_value < t1.min_value
)
and not exists (
select 1 from t t3
where t3.type = t1.type
and t3.next_max_value < t1.next_min_value
)
order by t1.type, group_id, t1.min_value;
The dense_rank() makes the group IDs contiguous; not sure if you actually need those at all, or if their sequence matters, so it's optional really. connect_by_root gives the group ID for the start of the chain, so although there were 12 rows and 12 group_id values in the initial query, they don't all appear in the final result.
The connection is via two prior values, type and the next set ID found in the initial query. That creates all the chains, but own its own would also include shorter chains - for d you'd see 8,9,10,11 but also 9,10,11 and 10,11, which you don't want as separate groups. Those are eliminated by the start with conditions, which could maybe be simplified.
That gives:
TYPE GROUP_ID SET_ID
---- -------- ------
a 1 1
a 1 2
a 2 1
a 2 3
a 3 4
a 3 3
b 4 5
c 5 6
c 6 7
d 7 8
d 7 9
d 7 10
d 7 11
SQL Fiddle demo.
If you can identify all the groups and their starting set_id then you can use a recursive approach and do this all in a single statement, rather than needing to populate a table iteratively. However you'd need to benchmark both approaches both for speed/efficiency and resource consumption - whether it will scale for your data volumes and within your system's available resources would need to be verified.
If I understand when you decide to start a new group you can identify them all at once with a query like:
with t as (
select t1.type, t1.set_id, t1.min_value, t1.max_value,
t2.set_id as next_set_id, t2.min_value as next_min_value,
t2.max_value as next_max_value
from table1 t1
left join table1 t2 on t2.type = t1.type and t2.min_value > t1.max_value
where not exists (
select 1
from table1 t3
where t3.type = t1.type
and t3.max_value < t1.min_value
)
)
select t.type, t.set_id, t.min_value, t.max_value,
t.next_set_id, t.next_min_value, t.next_max_value,
row_number() over (order by t.type, t.min_value, t.next_min_value) as grp_id
from t
where not exists (
select 1 from t t2
where t2.type = t.type
and t2.next_max_value < t.next_min_value
)
order by grp_id;
The tricky bit here is getting all three groups for a, specifically the two groups that start with set_id = 1, but only one group for d. The inner select (in the CTE) looks for sets that don't have a lower non-overlapping range via the not exists clause, and outer-joins to the same table to get the next set(s) that don't overlap, which gives you two groups that start with set_id = 1, but also four that start with set_id = 9. The outer select then ignores everything but the lowest non-overlapping with a second not exists clause - but doesn't have to hit the real table again.
So that gives you:
TYPE SET_ID MIN_VALUE MAX_VALUE NEXT_SET_ID NEXT_MIN_VALUE NEXT_MAX_VALUE GRP_ID
---- ------ ---------- ---------- ----------- -------------- -------------- ------
a 1 1 3 2 4 10 1
a 1 1 3 3 6 10 2
a 4 2 5 3 6 10 3
b 5 1 9 4
c 6 1 7 5
c 7 3 10 6
d 8 1 2 9 3 3 7
You can then use that as the anchor member in a recursive subquery factoring clause:
with t as (
...
),
r (type, set_id, min_value, max_value,
next_set_id, next_min_value, next_max_value, grp_id) as (
select t.type, t.set_id, t.min_value, t.max_value,
t.next_set_id, t.next_min_value, t.next_max_value,
row_number() over (order by t.type, t.min_value, t.next_min_value)
from t
where not exists (
select 1 from t t2
where t2.type = t.type
and t2.next_max_value < t.next_min_value
)
...
If you left the r CTE with that and just did sleect * from r you'd get the same seven groups.
The recursive member then uses the next set_id and its range from that query as the next member of each group, and repeats the outer join/not-exists look up to find the next set(s) again; stopping when there is no next non-overlapping set:
...
union all
select r.type, r.next_set_id, r.next_min_value, r.next_max_value,
t.set_id, t.min_value, t.max_value, r.grp_id
from r
left join table1 t
on t.type = r.type
and t.min_value > r.next_max_value
and not exists (
select 1 from table1 t2
where t2.type = r.type
and t2.min_value > r.next_max_value
and t2.max_value < t.min_value
)
where r.next_set_id is not null -- to stop looking when you reach a leaf node
)
...
Finally you have a query based on the recursive CTE to get the columns you want and to specify the order:
...
select r.type, r.grp_id, r.set_id
from r
order by r.type, r.grp_id, r.min_value;
Which gets:
TYPE GRP_ID SET_ID
---- ---------- ----------
a 1 1
a 1 2
a 2 1
a 2 3
a 3 4
a 3 3
b 4 5
c 5 6
c 6 7
d 7 8
d 7 9
d 7 10
d 7 11
SQL Fiddle demo.
If you wanted to you could show the min/max values for each set, and could track and show the min/max value for each group. I've just show then columns from the question though.

Oracle , select relation

I have 2 tables in oracle DB: Items and Relationship.
items:
ID
---
1
2
3
4
5
6
7
relationship:
ID parent child
--------------------
1 1 2
2 1 3
3 1 4
4 2 5
5 2 6
6 3 7
In the relationship table, I'm storing the hierarchial structure of the "items" (do not ask why it's stored in different tables).
The question:
When I execute this query:
SELECT PARENT_ID, CHILD_ID, CONNECT_BY_ISLEAF, MAX(LEVEL) OVER () + 1 - LEVEL as rev_level
FROM relationship
CONNECT BY PRIOR PARENT_ID = CHILD_ID
START WITH CHILD_ID = 7;
I do not see the root parent because he doesn't exist in this table as a child.
The question is how can I add the root parent(ID = 1) to the query result or join it whith the "items" table and keep the result columns (level and isleaf).
CONNECT_BY_ISLEAF works the other way around when using bottom up search (see this link http://technology.amis.nl/2009/11/14/oracle-11gr2-alternative-for-connect_by_isleaf-function-for-recursive-subquery-factoring-dedicated-to-anton/)
I assume that you want to show more data about the item (like name) If that is the case just left join the items table.
SELECT PARENT_ID AS PARENT_ID,CHILD_ID, i.name AS CHILD_NAME,
CONNECT_BY_ISLEAF,
MAX(LEVEL) OVER () + 1 - LEVEL AS rev_level
FROM items i
LEFT JOIN relationship r ON (i.id = r.CHILD_ID)
CONNECT BY PRIOR PARENT_ID = CHILD_ID
START WITH CHILD_ID = 7
ORDER BY REV_LEVEL;
check this SQLfiddle: http://sqlfiddle.com/#!4/5c9fa/17
In addition check this post about bottom up searches (http://bitbach.wordpress.com/2010/10/18/implementing-bottom-up-path-traversal-for-hierarchical-tables/)
Notice that you have both directions - parent and child.
pick one and dont mix the two.
1 with x as (
2 select 1 as id, 1 as parent, 2 as child from dual union all
3 select 2, 1 , 3 from dual union all
4 select 3 ,1, 4 from dual union all
5 select 4 ,2, 5 from dual union all
6 select 5 ,2, 6 from dual union all
7 select 6 ,3, 7 from dual)
8 select *
9 from x
10 sTART WITH child = 7
11* CONNECT BY PRIOR id= CHILD
SQL> /
ID PARENT CHILD
---------- ---------- ----------
6 3 7
5 2 6
4 2 5
3 1 4
2 1 3
1 1 2
connection is made by prior id = child and not prior parent = child