Find top parent of child, multiple levels - sql

ENTRY TABLE
__________________
| ID | PARENT_ID |
| 1 | null |
| 2 | 1 |
| 3 | 2 |
| 4 | null |
| 5 | 4 |
| 6 | 5 |
...
I make copies of the entries in some cases and they are conneted by parent ID.
Each entry can have one copy:
THIS WONT HAPPEN
__________________
| ID | PARENT_ID |
| 1 | null |
| 2 | 1 |
| 3 | 1 |
...
Sometimes I need to take a copy and query for it's top level parent. I need to find the top parent entries for all the entries I search for.
For example, if I query for the parents of ID 6 and 3, I would get ID 4 and 1.
If I query for the parents of ID 5 and 2, I would get ID 4 and 1.
But also If I query for ID 5 and 1, it should return ID 4 and 1 because the entry ID 1 is already the top parent itself.
I don't know where to begin since I don't know how to recursively query in such case.
Can anyone point me in the right direction?
I know that the query below will just return the child elemements (ID 6 and 3), but I don't know where to go from here honestly.
I am using OracleSQL by the way.
SELECT * FROM entry WHERE id IN (6, 3);

You can use a hierarchical query and CONNECT_BY_ROOT.
Either starting at the root of the hierarchy and working down:
SELECT id,
CONNECT_BY_ROOT(id) AS root_id
FROM entry
WHERE id IN (6, 3)
START WITH parent_id IS NULL
CONNECT BY PRIOR id = parent_id;
Or, from the entry back up to the root:
SELECT CONNECT_BY_ROOT(id) AS id,
id AS root_id
FROM entry
WHERE parent_id IS NULL
START WITH id IN (6, 3)
CONNECT BY PRIOR parent_id = id;
Which, for the sample data:
CREATE TABLE entry( id, parent_id ) 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, 5 FROM DUAL UNION ALL
SELECT 7, 6 FROM DUAL
Both output:
ID
ROOT_ID
3
1
6
4
db<>fiddle here

You can use recursive CTE to walk the graph and find the initial parent. For example:
with
n (starting_id, current_id, parent_id, v) as (
select id, id, parent_id, 0 from entry where id in (6, 3)
union all
select n.starting_id, e.id, e.parent_id, n.v - 1
from n
join entry e on e.id = n.parent_id
)
select starting_id, current_id as initial_id
from (
select n.*, row_number() over(partition by starting_id order by v) as rn
from n
) x
where rn = 1
Result:
STARTING_ID INITIAL_ID
------------ ----------
3 1
6 4
See running example at db<>fiddle.

Related

Oracle SQL hierarchical query from bottom to top

I have a table where I want to go from bottom to top using hierarchical queries.
The problem is that I need the get the value of one column from root (top) using CONNECT_BY_ROOT, but since I reverse the way the hierarchical query works (reverse the prior in connect by and the start with), this function (CONNECT_BY_ROOT) consider my 'start with' row as level 1 (root) then gets me this value.
In other words, I want a way to reverse the CONNECT_BY_ROOT to get me the value of a column from the last possible level and not the root.
+----+-----------+-------+
| ID | ID_PARENT | VALUE |
+----+-----------+-------+
| 1 | null | 5 |
| 2 | 1 | 9 |
| 3 | 2 | null |
+----+-----------+-------+
I want to get the value of ID = 1 (5) to the ID = 3 like this:
+----+-------+------------+
| ID | VALUE | VALUE_root |
+----+-------+------------+
| 1 | 5 | 5 |
| 2 | 9 | 5 |
| 3 | null | 5 |
+----+-------+------------+
I tried this but all I get is null as value_root:
SELECT id,
CONNECT_BY_ROOT VALUE as VALUE_root
FROM my_table
START WITH ID = 3
CONNECT BY ID = PRIOR ID_PARENT
EDIT: I forgot to mention that in my real system I'm dealing with millions of rows of data, the reason why I'm reversing the hierachical queries in first place is to make it better in terms of performance!
You may retrieve the root (which is a bottom node in your case) for all the tree upwards and then apply analytical function partitioned by the root to translate parent value to all the tree nodes. This is also possible for multiple nodes in start with.
with src (id, parentid, val) as (
select 1, cast(null as int), 5 from dual union all
select 2, 1, 9 from dual union all
select 3, 2, null from dual union all
select 4, 2, null from dual union all
select 5, null, 10 from dual union all
select 6, 5, 7 from dual
)
select
connect_by_root id as tree_id
, id
, parentid
, val
, max(decode(connect_by_isleaf, 1, val))
over(partition by connect_by_root id) as val_root
from src
start with id in (3, 4, 6)
connect by id = prior parentid
order by 1, 2, 3
TREE_ID
ID
PARENTID
VAL
VAL_ROOT
3
1
-
5
5
3
2
1
9
5
3
3
2
-
5
4
1
-
5
5
4
2
1
9
5
4
4
2
-
5
6
5
-
10
10
6
6
5
7
10
You can try below query here I have just updated the START WITH condition and CONNECT BY clause -
SELECT id,
CONNECT_BY_ROOT VALUE as VALUE_root
FROM my_table
START WITH ID = 1
CONNECT BY PRIOR ID = ID_PARENT;
Fiddle Demo.
You were almost there
SELECT id,
value,
CONNECT_BY_ROOT VALUE as VALUE_root
FROM your_table
START WITH ID = 1
CONNECT BY prior ID = ID_PARENT
One posibility is to first perform a hierarchical query starting from the root - to get the root node for each row.
In the second step you perform the bottom up query (starting in all leaves nodes) and use the pre-calculated root node
Below the solution using Recursive Subquery Factoring
with hir (id, id_parent, value, value_root) as
(select id, id_parent, value, value value_root
from tab
where id_parent is null
union all
select tab.id, tab.id_parent, tab.value, hir.value_root
from hir
join tab on tab.id_parent = hir.id
),
hir2 (id, id_parent, value, value_root) as
(select id, id_parent, value, value_root from hir
where ID in (select id from tab /* id of leaves */
minus
select id_parent from tab)
union all
select hir.id, hir.id_parent, hir.value, hir.value_root
from hir2
join hir on hir2.id_parent = hir.id
)
select id,value, value_root
from hir2
;
ID VALUE VALUE_ROOT
---------- ---------- ----------
3 5
2 9 5
1 5 5
Nte that the order of the row 3, 2, 1 is the bottom up order that you want, but fail to reach in your example output.

Creating a SQL view to query whether a node is a descendant of a specific node

I have 2 SQL tables with the following data structure:
Table 1 : FAVORITES
Columns:
pk
fk_user (the user table is irrelevant for now)
fk_tree_node
Table 2 : TREE
Columns:
pk
fk_parent_node
I want to create a view from so that I can query, whether a node is favorited/descendant of a favorited node or not. So for every entry in FAVORITES it the view would have several entries where the user is associated with either the favorited node, or a descendant of it.
View: FAV_OR_DESCENDANT
Columns:
fk_tree_node
fk_user
pk
Queries would work like this
SELECT *
FROM FAV_OR_DESCENDANT
WHERE fk_user = 0
The results I'd expect to get for a given tree would look like this:
Tree:
TREE:
+--------+----+----------------+
| rownum | pk | pk_parent_node |
+--------+----+----------------+
| 1 | 1 | null |
| 2 | 2 | 1 |
| 3 | 3 | 2 |
| 4 | 4 | 1 |
+--------+----+----------------+
FAVORITES:
+--------+----+---------+--------------+
| rownum | pk | fk_user | fk_tree_node |
+--------+----+---------+--------------+
| 1 | 0 | 0 | 1 |
+--------+----+---------+--------------+
Tree representation:
1 <-- User 0 has only favorited this single node
/ \
2 4
/
3
Result data in FAV_OR_DESCENDANT:
+--------+---------+--------------+
| rownum | fk_user | fk_tree_node |
+--------+---------+--------------+
| 1 | 0 | 1 |
| 2 | 0 | 2 |
| 3 | 0 | 3 |
| 4 | 0 | 4 |
+--------+---------+--------------+
I know how to write this query if I'm asking for all favorited nodes/descendant nodes for a specific user. However, I'm struggling in translating that into an SQL query that would create a view:
SELECT DISTINCT *
FROM tree
START WITH tree.pk IN (
SELECT fk_tree_node
FROM favorites
WHERE fk_user = 0
)
CONNECT BY PRIOR tree.pk = tree.fk_parent_node
Other questions I found where more centered around making queries or were not limited by SQL. I'd be thankful for every hint in the right direction.
Since you need to get all descendant nodes, you need to aggregate all parent nodes, for example using sys_connect_by_path (or you can use hierarchical recursive subquery factoring):
with
TREE(pk, pk_parent_node) as (
select 1 , null from dual union all
select 2 , 1 from dual union all
select 3 , 2 from dual union all
select 4 , 1 from dual
)
,FAVORITES( pk, fk_user, fk_tree_node) as (
select 0, 0, 1 from dual
)
,v_tree as (
select pk,pk_parent_node,sys_connect_by_path(pk,'/') p_path
from TREE
start with pk_parent_node is null
connect by prior pk = pk_parent_node
)
select *
from v_tree;
Results:
PK PK_PARENT_NODE P_PATH
---------- -------------- ------------------------------
1 NULL /1
2 1 /1/2
3 2 /1/2/3
4 1 /1/4
In fact you can already check if P_PATH contains a favorite node, but since you want a view with users, we can aggregate them into a new column:
with
TREE(pk, pk_parent_node) as (
select 1 , null from dual union all
select 2 , 1 from dual union all
select 3 , 2 from dual union all
select 4 , 1 from dual
)
,FAVORITES( pk, fk_user, fk_tree_node) as (
select 0, 0, 1 from dual union all
select 1, 1, 3 from dual
)
,v_tree as (
select pk,pk_parent_node,sys_connect_by_path(pk,'/')||'/' p_path
from TREE
start with pk_parent_node is null
connect by prior pk = pk_parent_node
)
select
v.*, v2.*
from v_tree v
outer apply(
select
xmlelement(USERS, xmlagg(xmlelement(ID, f.pk) order by f.pk)) as users
from FAVORITES f
where p_path like '%/'||f.fk_tree_node||'/%'
) v2;
Results:
PK PK_PARENT_NODE P_PATH USERS
---------- -------------- ------------------------------ ------------------------------------------------------------
1 NULL /1/ <USERS><ID>0</ID></USERS>
2 1 /1/2/ <USERS><ID>0</ID></USERS>
3 2 /1/2/3/ <USERS><ID>0</ID><ID>1</ID></USERS>
4 1 /1/4/ <USERS><ID>0</ID></USERS>
I've added one more user 1 to make it more clear.
So now you just need to add a predicate to filter users:
with
TREE(pk, pk_parent_node) as (
select 1 , null from dual union all
select 2 , 1 from dual union all
select 3 , 2 from dual union all
select 4 , 1 from dual
)
,FAVORITES( pk, fk_user, fk_tree_node) as (
select 0, 0, 1 from dual union all
select 1, 1, 3 from dual
)
,v_tree as (
select pk,pk_parent_node,sys_connect_by_path(pk,'/')||'/' p_path
from TREE
start with pk_parent_node is null
connect by prior pk = pk_parent_node
)
,v_final_view as (
select
v.*, v2.*
from v_tree v
outer apply(
select
xmlelement(USERS, xmlagg(xmlelement(ID, f.pk) order by f.pk)) as users
from FAVORITES f
where p_path like '%/'||f.fk_tree_node||'/%'
) v2
)
select *
from v_final_view
where
xmlexists(
'$USERS/USERS[ID=$USER_ID]'
passing
users as USERS,
1 as USER_ID -- your input param - user id
)
;
Results:
PK PK_PARENT_NODE P_PATH USERS
---- -------------- ------------ ------------------------------------
3 2 /1/2/3/ <USERS><ID>0</ID><ID>1</ID></USERS>
Of course, it's just an example of this approach, so you can use other functions for aggregation or even create materialized view for performance.

Sequence generation for new ID

I have a requirement where there are 2 tables A and B. There is a column named ID which is a primary key for A and foreign key for table B where the tables have one to many relationship between them. In table A for ID column we have entries 1,2,3 and for them we have corresponding multiple entries for one ID and another column in table B named SEQ where a sequence has been created for entries in it. Now for entries for ID 1, I have 3 entries in table B with SEQ 1,2,3 but when new ID entry will be there then i need the sequence again to start from 1 for that ID.**Can you please help me to do that.
TABLE A
I'd suggest you not to store the SEQ value. Why would you? It is easy to calculate it whenever needed. How? Like this, using row_number analytic function:
SQL> with b (id, name) as
2 (select 1, 'TRI' from dual union all
3 select 1, 'TRI' from dual union all
4 select 1, 'TRI' from dual union all
5 select 2, 'ROHIT' from dual union all
6 select 2, 'ROHIT' from dual union all
7 select 3, 'RAVI' from dual
8 )
9 select id,
10 name,
11 row_number() over (partition by id order by null) seq
12 from b;
ID NAME SEQ
---------- ----- ----------
1 TRI 1
1 TRI 2
1 TRI 3
2 ROHIT 1
2 ROHIT 2
3 RAVI 1
6 rows selected.
SQL>
If you still want to store it, now you know how.
Don't have multiple sequences. Just use a single sequence in the B table and accept that there will be gaps for each ID and then if you need sequential values you can calculate them as needed using the ROW_NUMBER() analytic function and, if you need to, put it in a view. Also, don't duplicate the name from table A in table B; keep your data normalised.
CREATE TABLE A (
id NUMBER(8,0)
GENERATED ALWAYS AS IDENTITY
CONSTRAINT A__id__pk PRIMARY KEY,
name VARCHAR2(20)
);
CREATE TABLE B (
id NUMBER(8,0)
CONSTRAINT B__id__nn NOT NULL
CONSTRAINT B__id__fk REFERENCES A(id),
seq NUMBER(8,0)
GENERATED ALWAYS AS IDENTITY
CONSTRAINT B__seq__pk PRIMARY KEY
);
Then you can create your sample data:
INSERT INTO A ( name )
SELECT 'TRI' FROM DUAL UNION ALL
SELECT 'ROHIT' FROM DUAL UNION ALL
SELECT 'RAVI' FROM DUAL;
INSERT INTO B ( id )
SELECT 1 FROM DUAL UNION ALL
SELECT 1 FROM DUAL UNION ALL
SELECT 2 FROM DUAL UNION ALL
SELECT 3 FROM DUAL UNION ALL
SELECT 2 FROM DUAL UNION ALL
SELECT 1 FROM DUAL;
And:
SELECT *
FROM B
Outputs:
ID | SEQ
-: | --:
1 | 1
1 | 2
2 | 3
3 | 4
2 | 5
1 | 6
If you want your output then create a view:
CREATE VIEW B_view ( id, name, seq ) AS
SELECT b.id,
a.name,
ROW_NUMBER() OVER ( PARTITION BY b.id ORDER BY seq )
FROM B
INNER JOIN A
ON ( B.id = A.id )
Then:
SELECT *
FROM b_view
Outputs:
ID | NAME | SEQ
-: | :---- | --:
1 | TRI | 1
1 | TRI | 2
1 | TRI | 3
2 | ROHIT | 1
2 | ROHIT | 2
3 | RAVI | 1
db<>fiddle here

Oracle Sql: Obtain a Sum of a Group, if Subgroup condition met

I have a dataset upon which I am trying to obain a summed value for each group, if a subgroup within each group meets a certain condition. I am not sure if this is possible, or if I am approaching this problem incorrectly.
My data is structured as following:
+----+-------------+---------+-------+
| ID | Transaction | Product | Value |
+----+-------------+---------+-------+
| 1 | A | 0 | 10 |
| 1 | A | 1 | 15 |
| 1 | A | 2 | 20 |
| 1 | B | 1 | 5 |
| 1 | B | 2 | 10 |
+----+-------------+---------+-------+
In this example I want to obtain the sum of values by the ID column, if a transaction does not contain any products labeled 0. In the above described scenario, all values related to Transaction A would be excluded because Product 0 was purchased. With the outcome being:
+----+-------------+
| ID | Sum of Value|
+----+-------------+
| 1 | 15 |
+----+-------------+
This process would repeat for multiple IDs with each ID only containing the sum of values if the transaction does not contain product 0.
Hmmm . . . one method is to use not exists for the filtering:
select id, sum(value)
from t
where not exists (select 1
from t t2
where t2.id = t.id and t2.transaction = t.transaction and
t2.product = 0
)
group by id;
Do not need to use correlated subquery with not exists.
Just use group by.
with s (id, transaction, product, value) as (
select 1, 'A', 0, 10 from dual union all
select 1, 'A', 1, 15 from dual union all
select 1, 'A', 2, 20 from dual union all
select 1, 'B', 1, 5 from dual union all
select 1, 'B', 2, 10 from dual)
select id, sum(sum_value) as sum_value
from
(select id, transaction,
sum(value) as sum_value
from s
group by id, transaction
having count(decode(product, 0, 1)) = 0
)
group by id;
ID SUM_VALUE
---------- ----------
1 15

Selecting the same row multiple times

I have a table that has some children of a master object. Any child can occur more than once, and there is a Occurences column that contains that number, so the data in the table is something like:
ChildID | ParentID | Occurences
-------------------------------
1 | 1 | 2
2 | 1 | 2
3 | 2 | 1
4 | 2 | 3
I need to get a list of all the children, with each child appearing the corect number of times in the result, something like
IDENT | ChildID | ParentID
--------------------------
1 | 1 | 1
2 | 1 | 1
3 | 2 | 1
4 | 2 | 1
5 | 3 | 2
6 | 4 | 2
7 | 4 | 2
8 | 4 | 2
I can do this with a cursor that loops the table and inserts as many rows as neccessary, but I don't think that that is the best solution possible.
Thanks for the help
Create script included:
DECLARE #Children TABLE (ChildID int, ParentID int, Occurences int)
INSERT #Children
SELECT 1, 1, 2 UNION ALL
SELECT 2, 1, 2 UNION ALL
SELECT 3, 2, 1 UNION ALL
SELECT 4, 2, 3
;with C as
(
select ChildID,
ParentID,
Occurences - 1 as Occurences
from #Children
union all
select ChildID,
ParentID,
Occurences - 1 as Occurences
from C
where Occurences > 0
)
select row_number() over(order by ChildID) as IDENT,
ChildID,
ParentID
from C
order by IDENT
;WITH CTEs
AS
(
SELECT 1 [Id]
UNION ALL
SELECT [Id] + 1 FROM CTEs WHERE [Id] < 100
)
SELECT ROW_NUMBER() OVER(ORDER BY c1.ChildID, c1.ParentID) [rn]
, c1.ChildID, c1.ParentID
FROM CTEs ct
JOIN #Children c1 ON c1.Occurences >= ct.[Id]
Another way to generate sequence is using predefined table, e.g. master.dbo.spt_values:
SELECT ROW_NUMBER() OVER(ORDER BY c1.ChildID, c1.ParentID) [rn]
, c1.ChildID, c1.ParentID
FROM master.dbo.spt_values ct
JOIN #Children c1 ON c1.Occurences > ct.number
AND ct.type = 'P'