SQL recursive query for complex table - sql

I have a pretty complex table structure with parent-child relations.
The idea behind the structure is that some object in child_id can trigger a parent_id.
Assume this data;
Table 1 - map
map_id | parent_id | child_id
1 | 1 | 2
2 | 1 | 3
3 | 1 | 4
Table 2 - attributes
attribute_id | child_id | id_to_trigger
1 | 2 | 5
2 | 5 | 6
Example: A questionnaire system is a master. It can contain sub groups to be answered; in which case the sub groups become child of the master. Some answers in the sub groups can trigger an additional sub group within it.
I want to now be able to fetch all the sub group id's for a given master. A sub group can be triggered from multiple sub groups but that isn't a problem since I need just the sub group id's.
As you can tell, master with id 1 has 3 sub groups 2, 3, 4. In the attributes table we can see that sub group 2 can trigger sub group 5; similarly 5 can trigger 6 and so on.
I need 2, 3, 4, 5, 6 in my output. How do i achieve this?

Think about your design, i suggest that you dont need 2 tables if you add these 2 recs to your table 1
map_id | parent_id | child_id
1 | 1 | 2
2 | 1 | 3
3 | 1 | 4
4 | 2 | 5
5 | 5 | 6
you can now use a standard CTE to walk the tree
like this
with Tree as (select child_id from table_1 where parent_id = 1
union all
select table_1.child_id from table_1
inner join Tree on Tree.child_id = table_1.parent_id)
select * from Tree
if you cant change schema this will work
with
table_1 as ( select Parent_id , child_id from map
union all
select child_id as Parent_id, id_to_trigger as child_id from attributes)
,Tree as (select child_id from table_1 where parent_id = 1
union all
select table_1.child_id from table_1
inner join Tree on Tree.child_id = table_1.parent_id)
select * from Tree

Try this :
SELECT
map.parent_id,
map.child_id
FROM
map
UNION
SELECT
attributes.child_id,
attributes.id_to_trigger
FROM
map
Inner JOIN attributes ON map.child_id = attributes.child_id
UNION
SELECT
T1.child_id,
T1.id_to_trigger
FROM
attributes T1
Inner JOIN attributes T2 ON T1.child_id = T2.id_to_trigger
Result :
parent_id | child_id
1 | 2
1 | 3
1 | 4
2 | 5
5 | 6

Related

Find top parent of child, multiple levels

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.

Find out what group id contains all relevant attributes in SQL

So lets say in this case, the group that we have is groups of animals.
Lets say I have the following tables:
animal_id | attribute_id | animal
----------------------------------
1 | 1 | dog
1 | 4 | dog
2 | 1 | cat
2 | 3 | cat
3 | 2 | fish
3 | 5 | fish
id | attribute
------------------
1 | four legs
2 | no legs
3 | feline
4 | canine
5 | aquatic
Where the first table contains the attributes that define an animal, and the second table keeps track of what each attribute is. Now lets say that we run a query on some data and get the following result table:
attribute_id
------------
1
4
This data would describe a dog, since it is the only animal_id that has both attributes 1 and 4. I want to be able to somehow get the animal_id (which in this case would be 1) based on the third table, which is essentially a table that has already been generated that contains the attributes of an animal.
EDIT
So the third table that has 1 and 4 doesn't have to be 1 and 4. It could return 2 and 5 (for fish), or 1 and 3 (cat). We can assume that it's result will always match one animal completely, but we don't know which one.
You can use group by and having:
with a as (
select 1 as attribute_id from dual union all
select 4 as attribute_id from dual
)
select t.animal_id, t.animal
from t join
a
on t.attribute_id = a.attribute_id
group by t.animal_id, t.animal
having count(*) = (select count(*) from a);
The above will find all animals that have those attributes and any others. If you want animals that have exactly those 2 attributes:
with a as (
select 1 as attribute_id from dual union all
select 4 as attribute_id from dual
)
select t.animal_id, t.animal
from t left join
a
on t.attribute_id = a.attribute_id
group by t.animal_id, t.animal
having count(*) = (select count(*) from a) and
count(*) = count(a.attribute_id);

Fill table with data based on other table?

I have 2 tables in Oracle database: document and document_closure.
document:
- id
- name
- parent_id
document_closure:
- id
- parent_id
- child_id
- level
document table has a lot of data (10k~20k). document_closure is empty.
Question: How to fill document_closure table with data based on document table. What sql script needs to be my that task?
Lets say I have such tree. Example:
A
|
- B
|
- C
document table:
id | parent_id | name
1 | | A
2 | 1 | B
3 | 2 | C
Finally document_closure must be:
id | parent_id | child_id | level
1 | 1 | 1 | 0
2 | 2 | 2 | 0
3 | 3 | 3 | 0
4 | 1 | 2 | 1
5 | 2 | 3 | 1
6 | 1 | 3 | 2
While parent_id and name fields in document_closure are the same coming from document table, you didn't mention what to put in the name field...
However assuming you can use the same (or null) value for the name field you just need an INSERT INTO SELECT statement like this
INSERT INTO table2 (column1, column2, column3, ...) SELECT column1, column2, column3, ... FROM table1 WHERE condition;
that copies data from document table and inserts them into document_closure table. In your case you just need the following SQL:
INSERT INTO document_closure (id,parent_id) SELECT id,parent_id FROM document;
and you'll have all your records in document table copied in document_closure table.
This can be done USING Oracle's connect by for hierarchical queries. This comes with a number of handy functions, including level to indicate how far down the hierarchy you are and connect_by_root() which returns the root value of the hierarchy (i.e. the top level value).
The query to generate the data based on the documents table looks something like:
WITH documents AS (SELECT 1 ID, NULL parent_id, 'A' NAME FROM dual UNION ALL
SELECT 2 ID, 1 parent_id, 'B' NAME FROM dual UNION ALL
SELECT 3 ID, 2 parent_id, 'C' NAME FROM dual)
-- end of mimicking a table with your sample data in it.
-- Since you already have this table, you don't need to bother defining the above subquery.
SELECT row_number() OVER (ORDER BY LEVEL, connect_by_root(ID), ID) ID,
connect_by_root(ID) parent_id,
ID child_id,
LEVEL -1 lvl
FROM documents d
CONNECT BY PRIOR ID = parent_id;
ID PARENT_ID CHILD_ID LVL
---------- ---------- ---------- ----------
1 1 1 0
2 2 2 0
3 3 3 0
4 1 2 1
5 2 3 1
6 1 3 2

SQL:How to dynamically return error code for records which doesn't exist in table

I am trying to replicate a workplace scenario. The sqlfiddle for Oracle db is not working so I couldn't recreate the table.
Say I have a table like below
Table1
+----+------+
| ID | Col1 |
+----+------+
| 1 | A |
| 2 | B |
| 3 | C |
+----+------+
Now we run a query with where condition. The in clause for where is passed by user and run time and can change.
Suppose user inputs 1,2,4,5
So the SQL will be like
select t.* from Table1 t where t.id in (1,2,4,5);
The result of this query will be
+----+------+
| ID | Col1 |
+----+------+
| 1 | A |
| 2 | B |
+----+------+
Now output I am expecting should be something like below
+----+---------+------+
| ID | ErrCode | Col1 |
+----+---------+------+
| 1 | 0 | A |
| 2 | 0 | B |
| 4 | 404 | |
| 5 | 404 | |
+----+---------+------+
As 3 was not entered by user, we will not return it. But for 4 and 5, there is no record in our table, so I want to create another dummy column which will contain error code. The data columns should be null.
It is not mandatory that the user input should go to in clause. We can use it anywhere in the query.
I am thinking of some way of splitting the input id and use them as rows. Then use them to do left join with Table1 to find the records which exists and doesn't exist in Table1 and use case on that to decide among 0 or 404 as error code.
Appreciate any other way we can do it by query.
Here it goes
SQL> WITH table_filter AS
2 (SELECT regexp_substr(txt, '[^,]+', 1, LEVEL) id
3 FROM (SELECT '1,2,4,5' AS txt FROM dual) -- User input here
4 CONNECT BY regexp_substr(txt, '[^,]+', 1, LEVEL) IS NOT NULL),
5 table1 AS -- Sample data
6 (SELECT 1 id,
7 'A' col1
8 FROM dual
9 UNION ALL
10 SELECT 2,
11 'B'
12 FROM dual
13 UNION ALL
14 SELECT 3,
15 'C'
16 FROM dual)
17 SELECT f.id,
18 CASE
19 WHEN t.id IS NULL THEN
20 404
21 ELSE
22 0
23 END AS err_code,
24 t.col1
25 FROM table_filter f
26 LEFT OUTER JOIN table1 t
27 ON t.id = f.id;
ID ERR_CODE COL1
---------------------------- ---------- ----
1 0 A
2 0 B
5 404
4 404
SQL>
Oracle Setup:
CREATE TABLE Table1 ( id, col1 ) AS
SELECT 1, 'A' FROM DUAL UNION ALL
SELECT 2, 'B' FROM DUAL;
Query:
SELECT i.COLUMN_VALUE AS id,
NVL2( t.col1, 0, 404 ) AS ErrCode,
t.col1
FROM TABLE( SYS.ODCINUMBERLIST( 1, 2, 4, 5 ) ) i
LEFT OUTER JOIN
Table1 t
ON ( i.COLUMN_VALUE = t.id );
Output:
ID ERRCODE COL1
-- ------- ----
1 0 A
2 0 B
4 404
5 404
The collection of ids can be built dynamically using PL/SQL or an external language and then passed as a bind variable. See my answer here for an example.

SQL To Find All Descendents

I have a data table like this
Entities:
ID | Parent_ID
1 | null
2 | 1
3 | 1
4 | 3
5 | 4
6 | 4
I'd like a sql expression that will return a row for every entity and a linear descendant, plus a row for null if the entity has no descendants. So given the above data my result set would be:
Entity | Descendant
1 | 2
1 | 3
1 | 4
1 | 5
1 | 6
2 | null
3 | 4
3 | 5
3 | 6
4 | 5
5 | null
6 | null
I tried using a common table expression to achieve this, and think it's the way to do it, given its ability to recurse, but I couldn't get my head wrapped around the spawning of many rows for a single parent.
with all_my_children (my_father,my_id,my_descendant,level)
as
(
select parent_id,id,null,0
from Entities
where id not in (select parent_id from entities)
union all
select e.parent_id,e.id,amc.my_id,amc.level+1
from Entities e
inner join all_my_children amc
on e.id = amc.my_father
WHERE ????? --How do I know when I'm done? and How do I keep repeating parents for each descendant?
)
select my_id, my_descendant from all_my_children
Thanks for your time.
Here's what you asked for
WITH TEMP AS
(
SELECT ID AS ENTITY, PID AS DESCENDANTS
FROM YPC_BI_TEMP.DBO.SV7104
WHERE PID IS NULL
UNION ALL
SELECT PID AS ENTITY, ID AS DESCENDANTS
FROM YPC_BI_TEMP.DBO.SV7104
WHERE PID IS NOT NULL
UNION ALL
SELECT PRNT.ENTITY, CHILD.ID AS DESCENDANTS
FROM YPC_BI_TEMP.DBO.SV7104 AS CHILD
INNER JOIN TEMP AS PRNT
on PRNT.DESCENDANTS = CHILD.PID
--AND PRNT.ENTITY IS NOT NULL
)
SELECT DISTINCT ENTITY, DESCENDANTS FROM TEMP
UNION
SELECT ID AS ENTITY, NULL AS DESCENDANTS FROM YPC_BI_TEMP.DBO.SV7104
WHERE ID NOT IN (SELECT ENTITY FROM TEMP)
Deleted my previous answer, but I think this might do the trick...
WITH all_my_children AS (my_father,my_id,my_descendant,level)
(
SELECT parent_id, id, null, 0
FROM Entities
WHERE parent_id IS NULL -- the roots of your tree
UNION ALL
SELECT COALESCE(e2.parent_id, e.parent_id), e.id, amc.my_id, amc.level+1
FROM Entities e
JOIN all_my_children amc
ON e.parent_id = amc.my_id
LEFT JOIN Entities e2
ON e.id = e2.parent_id
)
SELECT * FROM all_my_children