Finding downstream member of tree within SQL Hierarchy Query? - sql

I have a binary tree with devices attached to nodes (connected by an adjacency list). I'm trying to compare a device's value to the device downstream of it. I'm having trouble getting the downstream device.
Let's say I have a table:
DEVICE
NODE
PARENT_NODE
LEVEL
1
a
null
1
null
b
a
2
null
c
b
3
2
d
c
4
3
e
d
5
9
m
b
3
null
n
m
4
7
o
n
5
How would I go about joining the closest downstream device to each device row? I'm expecting:
DEVICE
DOWNSTREAM_DEVICE
1
null
2
1
3
2
9
1
7
9
Some assumptions: There is no order to the devices or the nodes (assume they're both unique ids). LEVEL is the hierarchy level. I'm using Oracle SQL.
I thought I could just use a lag function to perform this query, but obviously this will not work due to the tree branching. My incorrect results are as follows:
DEVICE
DOWNSTREAM_DEVICE
1
null
2
1
3
2
9
3 <- wrong
7
9
Any leads would be appreciated.

Use a hierarchical query:
SELECT CONNECT_BY_ROOT device AS device,
CASE WHEN LEVEL = 1 THEN NULL ELSE device END AS downstream_device
FROM table_name
WHERE CONNECT_BY_ISLEAF = 1
START WITH device IS NOT NULL
CONNECT BY PRIOR parent_node = node
AND (LEVEL = 2 OR PRIOR device IS NULL)
Which, for the sample data:
CREATE TABLE table_name (DEVICE, NODE, PARENT_NODE, LVL) AS
SELECT 1, 'a', null, 1 FROM DUAL UNION ALL
SELECT null, 'b', 'a', 2 FROM DUAL UNION ALL
SELECT null, 'c', 'b', 3 FROM DUAL UNION ALL
SELECT 2, 'd', 'c', 4 FROM DUAL UNION ALL
SELECT 3, 'e', 'd', 5 FROM DUAL UNION ALL
SELECT 9, 'm', 'b', 3 FROM DUAL UNION ALL
SELECT null, 'n', 'm', 4 FROM DUAL UNION ALL
SELECT 7, 'o', 'n', 5 FROM DUAL;
Outputs:
DEVICE
DOWNSTREAM_DEVICE
1
null
2
1
3
2
9
1
7
9
fiddle

Related

Oracle SQL Grouping In Ranges

I am looking for ideas on how to group numbers into low and high ranges in Oracle SQL. I looking to to avoid cursors...any ideas welcome
Example input
ID
LOW
HIGH
A
0
2
A
2
3
A
3
5
A
9
11
A
11
13
A
13
15
B
0
1
B
1
4
B
7
9
B
11
12
B
12
17
B
17
18
Which would result in the following grouping into ranges
ID
LOW
HIGH
A
0
5
A
9
15
B
0
4
B
7
9
B
11
18
This is a Gaps & Islands problem. You can use the traditional solution.
For example:
select max(id) as id, min(low) as low, max(high) as high
from (
select x.*, sum(i) over(order by id, low) as g
from (
select t.*,
case when low = lag(high) over(partition by id order by low)
and id = lag(id) over(partition by id order by low)
then 0 else 1 end as i
from t
) x
) y
group by g
Result:
ID LOW HIGH
--- ---- ----
A 0 5
A 9 15
B 0 4
B 7 9
B 11 18
See running example at db<>fiddle.
From Oracle 12, you should use MATCH_RECOGNIZE for row-by-row pattern matching:
SELECT *
FROM table_name
MATCH_RECOGNIZE(
PARTITION BY id
ORDER BY low, high
MEASURES
FIRST(low) AS low,
MAX(high) AS high
PATTERN (overlapping* last_row)
DEFINE
overlapping AS NEXT(low) <= MAX(high)
)
Which, for the sample data:
CREATE TABLE table_name (id, low, high) AS
SELECT 'A', 0, 2 FROM DUAL UNION ALL
SELECT 'A', 2, 3 FROM DUAL UNION ALL
SELECT 'A', 3, 5 FROM DUAL UNION ALL
SELECT 'A', 9, 11 FROM DUAL UNION ALL
SELECT 'A', 11, 13 FROM DUAL UNION ALL
SELECT 'A', 13, 15 FROM DUAL UNION ALL
SELECT 'B', 0, 1 FROM DUAL UNION ALL
SELECT 'B', 1, 4 FROM DUAL UNION ALL
SELECT 'B', 7, 9 FROM DUAL UNION ALL
SELECT 'B', 11, 12 FROM DUAL UNION ALL
SELECT 'B', 12, 17 FROM DUAL UNION ALL
SELECT 'B', 17, 18 FROM DUAL UNION ALL
SELECT 'C', 0, 10 FROM DUAL UNION ALL
SELECT 'C', 1, 3 FROM DUAL UNION ALL
SELECT 'C', 5, 8 FROM DUAL UNION ALL
SELECT 'C', 9, 15 FROM DUAL UNION ALL
SELECT 'C', 10, 14 FROM DUAL UNION ALL
SELECT 'C', 11, 13 FROM DUAL;
Outputs:
ID
LOW
HIGH
A
0
5
A
9
15
B
0
4
B
7
9
B
11
18
C
0
15
fiddle

recursive query to find the root parent in oracle

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.

SQL Query to get all the ancestors of a node , in a single row

I have a table with the following structure
Child_id
Parent_Id
Child_name
Parent_name
Child_Description
I want a query to get all the parents of all leaf level nodes in a single row.
For eg : If X and Y are leaf level nodes in the following:
A->B->C->D->X
F->G->H->I->Y
The query should return 2 rows as following
Child Parent1 Parent2 Parent3 Parent4
X D C B A
Y I H G F
Thanks,
Dev
Does this make any sense?
SQL> with test (child_id, parent_id) as
2 (select 'x', 'd' from dual union all
3 select 'd', 'c' from dual union all
4 select 'c', 'b' from dual union all
5 select 'b', 'a' from dual union all
6 select 'a', null from dual union all
7 --
8 select 'y', 'i' from dual union all
9 select 'i', 'h' from dual union all
10 select 'h', 'g' from dual union all
11 select 'g', 'f' from dual union all
12 select 'f', null from dual
13 ),
14 anc as
15 (select sys_connect_by_path(child_Id, '>') pth
16 from test
17 where connect_by_isleaf = 1
18 connect by prior child_id = parent_id
19 start with parent_id is null
20 )
21 select regexp_substr(pth, '[^>]+', 1, 5) c1,
22 regexp_substr(pth, '[^>]+', 1, 4) c2,
23 regexp_substr(pth, '[^>]+', 1, 3) c3,
24 regexp_substr(pth, '[^>]+', 1, 2) c4,
25 regexp_substr(pth, '[^>]+', 1, 1) c5
26 from anc;
C1 C2 C3 C4 C5
-- -- -- -- --
x d c b a
y i h g f
SQL>
What does it do?
test CTE simulates your data (at least, I think so)
anc(estors) CTE selects the "longest" path because CONNECT_BY_ISLEAF shows whether current row can (or can not) be expanded any further. If it returns 1, it can not.
the final query uses regular expressions to convert a CSV string (delimiter is > in my example; could be something else) into columns. There's nothing dynamic in it, so - if data you have is different from up to 5 "columns", you'd have to fix it

Flag individuals that share common features with Oracle SQL

Consider the following table:
ID Feature
1 1
1 2
1 3
2 3
2 4
2 6
3 5
3 10
3 12
4 12
4 18
5 10
5 30
I would like to group the individuals based on overlapping features. If two of these groups again have overlapping features, I would consider both as one group. This process should be repeated until there are no overlapping features between groups. The result of this procedure on the table above would be:
ID Feature Flag
1 1 A
1 2 A
1 3 A
2 3 A
2 4 A
2 6 A
3 5 B
3 10 B
3 12 B
4 12 B
4 18 B
5 10 B
5 30 B
So actually the problem I am trying to solve is finding connected components in a graph. Here [1,2,3] is the graph with ID 1 (see https://en.wikipedia.org/wiki/Connectivity_(graph_theory)). The problem is equivalent to this problem, however I would like to solve it with Oracle SQL.
Here is one way to do this, using a hierarchical ("connect by") query. The first step is to extract the initial relationships from the base data; the hierarchical query is built on the result from this first step. I added one more row to the inputs to illustrate a node that is a connected component by itself.
You marked the connected components as A and B - of course, that won't work if you have, say, 30,000 connected components. In my solution, I use the minimum node name as the marker for each connected component.
with
sample_data (id, feature) as (
select 1, 1 from dual union all
select 1, 2 from dual union all
select 1, 3 from dual union all
select 2, 3 from dual union all
select 2, 4 from dual union all
select 2, 6 from dual union all
select 3, 5 from dual union all
select 3, 10 from dual union all
select 3, 12 from dual union all
select 4, 12 from dual union all
select 4, 18 from dual union all
select 5, 10 from dual union all
select 5, 30 from dual union all
select 6, 40 from dual
)
-- select * from sample_data; /*
, initial_rel(id_base, id_linked) as (
select distinct s1.id, s2.id
from sample_data s1 join sample_data s2
on s1.feature = s2.feature and s1.id <= s2.id
)
-- select * from initial_rel; /*
select id_linked as id, min(connect_by_root(id_base)) as id_group
from initial_rel
start with id_base <= id_linked
connect by nocycle prior id_linked = id_base and id_base < id_linked
group by id_linked
order by id_group, id
;
Output:
ID ID_GROUP
------- ----------
1 1
2 1
3 3
4 3
5 3
6 6
Then, if you need to add the ID_GROUP as a FLAG to the base data, you can do so with a trivial join.

Retrieve Parents and Child Rows In Single Row

I have a lot of SQL experience but this one is beyond my understanding. I have a table such as:
ID NAME PARENT_ID
1 A 0
2 B 1
3 C 1
4 D 0
5 E 4
6 F 4
7 G 4
And I need to write a single SQL statement that will return all the parents and their children in a single row:
ID PARENT CHILD_1 CHILD_2 CHILD_3 ... CHILD_N
1 A B C
4 D E F G
I do not know how many children each parent has before hand - it is variable.
Is there anyway to do this in a single SQL statement?
Thanks.
You can use some of the very cool "dynamic pivot" PL/SQL solutions out there (which I don't recommend for production code -- they work 99% but fail for some odd-ball cases, in my experience).
Otherwise, you need to tell Oracle ahead of time which columns you expect your SQL to output. That means, you can only do what you're looking to do if you implement a hard cap on the maximum number of child columns you'll include.
If you can live with having to do that, then this should work. I took some guesses about how you would want it to work if your data had a hierarchy with multiple levels. (Take a look at row "H" in the sample data and think about how you would want that displayed.)
WITH d AS (
SELECT 1 id, 'A' name, 0 parent_id
FROM DUAL UNION ALL
SELECT 2, 'B', 1
FROM DUAL UNION ALL
SELECT 3, 'C', 1
FROM DUAL UNION ALL
SELECT 4, 'D', 0
FROM DUAL UNION ALL
SELECT 5, 'E', 4
FROM DUAL UNION ALL
SELECT 6, 'F', 4
FROM DUAL UNION ALL
SELECT 7, 'G', 4
FROM DUAL UNION ALL
SELECT 8, 'H', 7
FROM DUAL
),
h as (
select prior d.name parent,
level lvl,
case when level = 1 then null else d.name end child_name,
case when level = 1 then null else row_number() over ( partition by prior d.name, level order by d.name) end child_Number
from d
connect by parent_id = prior id
start with parent_id = 0 )
select * from h
pivot ( max(child_name) for (child_number) in (1 AS "CHILD_1",2 AS "CHILD_2",3 AS "CHILD_3",4 AS "CHILD_4",5 AS "CHILD_5") )
where lvl > 1
order by parent;