How to get a recursive tree for a single table element - sql

I have a table of this type
| id | parent_id | | title |
parent_id refers to the id of the same table
I need to get a recursive tree for an element knowing only its parent.
it will be clearer what I mean in the picture
On the picture i need to get recursive parent tree for element E, (ะก id is known) i need get A - C - E tree without B and D and other elements, only for my element E
The nesting can be even greater, I just need to get all the parents in order without unnecessary elements.
This is needed for bread crumbs on my website
How i can do this in PostgreSQL?

Use RECURSIVE query
with recursive rec(id,parent_id, title) as (
select id,parent_id, title from t
where title = 'E'
union all
select t.*
from rec
join t on t.id = rec.parent_id
)
select * from rec
id|parent_id|title|
--+---------+-----+
5| 3|E |
3| 1|C |
1| |A |

Join your table on herself
SELECT t1.title, t2.title as parent, t3.title as great_parent, ...
FROM my_table t1
JOIN my_table t2 on t1.parent_id = t2.id
JOIN my_table t3 on t2.parent_id = t3.id
...
WHERE t1.title = 'curent'
if you don't know how many parent you have, use LEFT JOIN and do as mutch column as needed
thanks to Marmite Bomber
and with a small improvement to know the kinship level :
--drop table if exists recusive_test ;
create table recusive_test (id_parent integer, id integer, title varchar);
insert into recusive_test (id_parent , id , title) values
(1, 2, 'A')
,(2, 3, 'B')
,( 2, 4, 'C')
,( 4, 5, 'D')
,( 3, 6, 'E')
,( 3, 7, 'F')
,( 6, 8, 'G')
,( 6, 9, 'H')
,( 4, 10, 'I')
,( 4, 11, 'J');
WITH RECURSIVE search_tree(id, id_parent, title, step) AS (
SELECT t.id, t.id_parent, t.title ,1
FROM recusive_test t
where title = 'I'
UNION ALL
SELECT t.id, t.id_parent, t.title, st.step+1
FROM recusive_test t, search_tree st
WHERE t.id = st.id_parent
)
SELECT * FROM search_tree ORDER BY step DESC;

Related

Get root and top level from SQL tree

I have a tree table. And, I am going to get root and top level on this tree.
Help with the solution you can use anything you want
declare #disc table (
id int,
parent int,
label varchar(50)
)
insert into #disc
select *
from (
values (1, null, 'q_1'),
(2, 1, 'a_1_1'),
(3, 2, 'a_1_1_1'),
(4, 1, 'a_1_2'),
(5, null, 'q_5'),
(6, 5, 'a_5_1'),
(7, 5, 'a_5_2')
) x (id, parent, label);
1. q_1
2. a_1_1
3. a_1_1_1
4. a_1_2
5. q_5
6. a_5_1
7. a_5_2
And, my result should be like this:
1: 1, null, q_1
2: 2, 1, a_1_1
3: 5, null, q_5
4: 6, 5, a_5_1
or
1: 1, null, q_1
2: 5, null, q_5
3: 2, 1, a_1_1
4: 6, 5, a_5_1
I only found one way, but I believe there is a better solution:
with rec as (
select id, parent, label,
row_number() over(order by id) rnk,
1 lvl
from #disc
where parent is null
union all
select d.id, d.parent, d.label,
row_number() over(order by d.id) rnk,
r.lvl + 1
from rec r
join #disc d on r.id = d.parent
)
select *
from rec
where parent is null or (rnk = 1 and lvl = 2)
If I understand this, the parent value will be null in the root nodes. The next level down will have a root node as parent. So ...
;with roots as
(
select id, parent, label
from #disc
where parent is null
)
select id, parent, label
from roots
union
select id, parent, label
from #disc
where parent in (select id from roots)
It doesn't look like you actually want to recurse here.
You can just do a self-join inside an apply.
select
row_number() over (order by isnull(c.parent, c.id), c.id),
c.id,
c.parent,
c.label
from #disc p
cross apply (
select p.id, p.parent, p.label
union all
select top 1 c.id, c.parent, c.label
from #disc c
where p.id = c.parent
order by c.id
) c
where p.parent is null;
db<>fiddle

Oracle SQL Query - Element containing every element in subquery

I have 3 tables like so :
Document(ID:integer, Title:string)
Keywords(ID:integer, Name:string)
Document_Keywords(DocumentID:integer, KeywordID:integer)
Document_Keywords.DocumentID referencing Document.ID
Document_Keywords.KeywordID referencing Keywords.ID
A document contains [0, n] keywords.
I want to get every Document which Keywords contains at least a set of another Document's Keywords. As so:
Foo, Bar and Fred-> Documents
Foo's keywords: {1, 2, 3}
Bar's keywords: {1, 2, 3, 4}
Fred's keywords: {1, 3, 5}
If we search for all the documents keywords containing Foo's keywords, we get Bar but not Fred.
Here is the query I have so far:
SELECT KeywordID
FROM Document_Keywords DK
JOIN Document D ON D.ID = DK.DocumentID
WHERE D.title = 'Foo'
MINUS
SELECT KeywordID
FROM Document_Keywords
WHERE DocumentID = 1;
It returns an empty table if the Document with ID = 1 keywords contains at least every keywords of Foo's.
I can't find any other ways to solve this probleme as I can only use Oracle SQL to answer it.
If you want to get keywords with documents:
SELECT KeywordID, D1.ID DOC_ID, D1.Title
FROM Document_Keywords DK1
JOIN Document D1
on DK1.DocumentID = D1.ID
WHERE exists
(select 1
from Document D2
join Document_Keywords DK2
on D2.ID = DK2.DocumentID
where D2.title = 'Foo'
and DK1.KeywordID=DK2.KeywordID
and D1.ID!= D2.ID
);
Full test case with test data and results:
with
Document(ID, Title) as (
select 1, 'Foo' from dual union all
select 2, 'Bar' from dual union all
select 3, 'Fred' from dual
)
,Keywords(ID, Name) as (
select level, 'Key'||level from dual connect by level<=5
)
,Document_Keywords(DocumentID, KeywordID) as (
select 1, column_value from table(sys.odcinumberlist(1,2,3)) union all -- Foo's keywords: {1, 2, 3}
select 2, column_value from table(sys.odcinumberlist(1,2,3,4)) union all -- Bar's keywords: {1, 2, 3, 4}
select 3, column_value from table(sys.odcinumberlist(1,3,5)) -- Fred's keywords: {1, 3, 5}
)
SELECT KeywordID, D1.ID DOC_ID, D1.Title
FROM Document_Keywords DK1
JOIN Document D1
on DK1.DocumentID = D1.ID
WHERE exists
(select 1
from Document D2
join Document_Keywords DK2
on D2.ID = DK2.DocumentID
where D2.title = 'Foo'
and DK1.KeywordID=DK2.KeywordID
and D1.ID!= D2.ID
);
KEYWORDID DOC_ID TITLE
---------- ---------- -----
1 2 Bar
1 3 Fred
2 2 Bar
3 2 Bar
3 3 Fred
If you want without documents, just list of keywords:
SELECT distinct KeywordID
FROM Document_Keywords DK1
WHERE exists
(select 1
from Document D2
join Document_Keywords DK2
on D2.ID = DK2.DocumentID
where D2.title = 'Foo'
and DK1.KeywordID=DK2.KeywordID
and DK1.DocumentID!= D2.ID
);
Full tests case with the results:
with
Document(ID, Title) as (
select 1, 'Foo' from dual union all
select 2, 'Bar' from dual union all
select 3, 'Fred' from dual
)
,Keywords(ID, Name) as (
select level, 'Key'||level from dual connect by level<=5
)
,Document_Keywords(DocumentID, KeywordID) as (
select 1, column_value from table(sys.odcinumberlist(1,2,3)) union all -- Foo's keywords: {1, 2, 3}
select 2, column_value from table(sys.odcinumberlist(1,2,3,4)) union all -- Bar's keywords: {1, 2, 3, 4}
select 3, column_value from table(sys.odcinumberlist(1,3,5)) -- Fred's keywords: {1, 3, 5}
)
SELECT distinct KeywordID
FROM Document_Keywords DK1
WHERE exists
(select 1
from Document D2
join Document_Keywords DK2
on D2.ID = DK2.DocumentID
where D2.title = 'Foo'
and DK1.KeywordID=DK2.KeywordID
and DK1.DocumentID!= D2.ID
);
KEYWORDID
----------
1
2
3
If I have this right, you want documents whose keywords contain all of Fred's keywords as a submultiset.
Setup (building on Sayan's example):
create or replace type number_tt as table of number;
create table documents(id, title) as
select 1, 'Foo' from dual union all
select 2, 'Bar' from dual union all
select 3, 'Fred' from dual;
create table document_keywords(documentid, keywordid) as
select 1, column_value from table(number_tt(1,2,3)) union all
select 2, column_value from table(number_tt(1,2,3,4)) union all
select 3, column_value from table(number_tt(1,3,5))
Query:
with document_keywords_agg(documentid, title, keywordlist, keywordids) as (
select d.id, d.title
, listagg(dk.keywordid, ', ') within group (order by dk.keywordid)
, cast(collect(dk.keywordid) as number_tt)
from documents d
join document_keywords dk on dk.documentid = d.id
group by d.id, d.title
)
select dk1.documentid, dk1.title, dk1.keywordlist
, dk2.title as subset_title
, dk2.keywordlist as subset_keywords
from document_keywords_agg dk1
join document_keywords_agg dk2
on dk2.keywordids submultiset of dk1.keywordids
where dk2.documentid <> dk1.documentid;
Results:
DOCUMENTID
TITLE
KEYWORDLIST
SUBSET_TITLE
SUBSET_KEYWORDS
2
Bar
1, 2, 3, 4
Foo
1, 2, 3
To extend the example a little, let's add another document 'Dino' containing keywords {1,3,5,9}:
insert all
when rownum = 1 then into documents values (docid, 'Dino')
when 1=1 then into document_keywords values (docid, kw)
select 4 as docid, column_value as kw from table(number_tt(1,3,5,9));
Now the results are:
DOCUMENTID
TITLE
KEYWORDLIST
SUBSET_TITLE
SUBSET_KEYWORDS
2
Bar
1, 2, 3, 4
Foo
1, 2, 3
4
Dino
1, 3, 5, 9
Fred
1, 3, 5
(Add a filter to the where clause if you just want to check one document.)
SQL Fiddle
So, inner joining Document_Keyword to itself on KeywordID gives you the raw materials for what you are looking for, no?
. . .
From Document_Keywords A Inner Join Document_Keywords B On A.KeywordID=B.KeywordID
And A.DocumentID<>B.DocumentID
. . .
Granted, if the same Keyword is in multiple other documents you will get multiple occurrences of A.*, but you can summarize those out with a Group By, or possibly a Distinct clause.
If you need text-y results, you can add Document and Keywords table joins to this on the table A keys.
A query that delivers results in the format you specified above would be:
Select Title, ListAgg(KeywordID,',') Within Group (Order By KeywordID) as KeyWord_IDs
From (
Select D.Title,D.ID,A.KeywordID
From Document_Keywords A Inner Join Document_Keywords B On A.KeywordID=B.KeywordID
And A.DocumentID<>B.DocumentID
Inner Join Document D on D.ID=A.DocumentID
Group By A.DocumentID,A.KeyWordID
)
Group By Title,ID

How do I find the final "link in the chain" using a recursive CTE

I'm close on this but missing something. How do I only get the first and last links in chains such as A->B, B->C? How do I just get A->C?
CREATE TEMP TABLE IF NOT EXISTS chains (
cname TEXT PRIMARY KEY,
becomes TEXT
);
INSERT INTO chains
VALUES
('A', NULL),
('B', 'C'),
('C', 'D'),
('D', 'E'),
('E', NULL)
;
WITH RECURSIVE
final_link AS (
SELECT
chains.cname,
chains.becomes
FROM
chains
UNION
SELECT
chains.cname,
final_link.becomes
FROM
chains
INNER JOIN final_link
ON chains.becomes = final_link.cname
)
SELECT * FROM final_link;
The results I would like are:
cname | becomes
------|--------
'B' | 'E'
'C' | 'E'
'D' | 'E'
Here is one approach:
with recursive final_link as (
select cname, becomes, cname original_cname, 0 lvl
from chains
where becomes is not null
union all
select c.cname, c.becomes , f.original_cname, f.lvl + 1
from chains c
inner join final_link f on f.becomes = c.cname
where c.becomes is not null
)
select distinct on (original_cname) original_cname, becomes
from final_link
order by original_cname, lvl desc
The idea is to have the subquery keep track of the starting node, and of the level of each node in the tree. You can then filter with distinct on in the outer query.
Demo on DB Fiddle:
original_cname | becomes
:------------- | :------
B | E
C | E
D | E
You can achieve this by starting the recursion only with the chain ends, not with all links, then iteratively prepending links as you are already doing:
WITH RECURSIVE final_link AS (
SELECT cname, becomes
FROM chains c
WHERE (SELECT becomes IS NULL FROM chains WHERE cname = c.becomes)
UNION
SELECT c.cname, fl.becomes
FROM chains c
INNER JOIN final_link fl ON c.becomes = fl.cname
)
SELECT * FROM final_link;
(Demo)

Select Parent having null and not null child

Given 3 tables like:
[Table_Main] ----> [Table_Sub] ----> [Table_Prop]
1-N 0-N
I want to select item in [Table_Main] that :
- Have multiple [Table_Sub].
- with [Table_Sub] lines that have both [Table_Prop] and haven't.
To select those value I use :
SELECT Table_Main.Field_ID
FROM Table_Main
INNER JOIN Table_Sub on Table_Main.Field_ID = Table_Sub.Table_Main_Field_ID
LEFT JOIN Table_Prop on Table_Sub.Field_ID = Table_Prop.Table_Sub_Field_ID
If we rename table Family, Child and Pet. I need family where some childs has pet(s) but some child doesn't.
Family: Id, Name
1, Foo -- Family with 2 childs, one of them has a pet
2, Bar -- Family with 2 childs, 0 pet
3, Abc -- Family with 2 childs, both have pet
Child: Id, Family_Id, Name
1, 1, John -- Child of Foo
2, 1, Joe -- Child of Foo
3, 2, Jane
4, 2, Jessica
5, 3, XXX
6, 3, YYY
Pet: Id, Child_Id, Name
1, 2, FooBar -- Joe's pet
2, 5, Huey
3, 6, Dewey
Expected Result:
1, Foo
Family with less than 2 childs is exclude from the exemple has they can satisfy both constraint:
- Has a child with a pet
- Has a child with no pet.
Table Creation :
CREATE TABLE Family(
1 INTEGER NOT NULL PRIMARY KEY
,Foo VARCHAR(20) NOT NULL
);
INSERT INTO Family(1,Foo) VALUES (1,'Foo');
INSERT INTO Family(1,Foo) VALUES (2,'Bar');
INSERT INTO Family(1,Foo) VALUES (3,'Abc');
CREATE TABLE Child(
Id INTEGER NOT NULL PRIMARY KEY
,Family_Id INTEGER NOT NULL
,Name VARCHAR(20) NOT NULL
);
INSERT INTO Child(Id,Family_Id,Name) VALUES (1,1,'John');
INSERT INTO Child(Id,Family_Id,Name) VALUES (2,1,'Joe');
INSERT INTO Child(Id,Family_Id,Name) VALUES (3,2,'Jane');
INSERT INTO Child(Id,Family_Id,Name) VALUES (4,2,'Jessica');
INSERT INTO Child(Id,Family_Id,Name) VALUES (5,3,'XXX');
INSERT INTO Child(Id,Family_Id,Name) VALUES (6,3,'YYY');
CREATE TABLE Pet(
Id INTEGER NOT NULL PRIMARY KEY
,Family_I INTEGER NOT NULL
,Name VARCHAR(20) NOT NULL
);
INSERT INTO Pet(Id,Family_Id,Name) VALUES (1,2,'FooBar');
INSERT INTO Pet(Id,Family_Id,Name) VALUES (2,5,'Huey');
INSERT INTO Pet(Id,Family_Id,Name) VALUES (3,6,'Dewey');
This will give you desired result.
;with family as
(
select 1 FamilyID, 'Foo' Family union select 2, 'Bar' union select 3, 'ABC'
), child as
(
select 1 ChildID, 1 FamilyID ,'John' ChildName union
select 2, 1, 'Joe' union
select 3, 2, 'Jane' union
select 4, 2, 'Jessica' union
select 5, 3, 'XXX'union
select 6, 3, 'YYY'
), pets as
(
select 1 petid , 2 childid, 'FooBar' pet union
select 2, 5, 'Huey' union
select 3, 6, 'Dewey'
)
SELECT T.FamilyID, Max(Family) Family, MIN(CNT) [Min] , MAX(CNT) [Max] FROM
(
SELECT f.FamilyID, C.ChildID, SUM(case when petid is null then 0 else 1 end) CNT FROM Family F
JOIN Child C ON F.FamilyID = C.FamilyID
LEFT JOIN Pets P ON C.ChildID = P.ChildID
GROUP BY F.FamilyID, C.ChildID
) T JOIN Family F on T.FamilyID = F.FamilyID GROUP BY T.FamilyID
HAVING MIN(CNT) = 0 AND MAX(CNT) > 0
Query
select family.ID, family.name
from family
left join child on family.ID = child.family_id
left join pet on pet.child_ID = child.Id
group by family.name,family.ID
having count(child.id) > 1 and count( pet.id) <>0 and count(child.id) > count( pet.id)
Output
looks like you are close but if I understand right:
With parent as (
select 'Charlie' name from dual union all
select 'Ben' name from dual union all
select 'Bob' name from dual union all
select 'Harry' name from dual
)
,child as (
select 'Ben' parentname, 'Bebbie' name from dual union all
select 'Ben' parentname, 'Tilda' name from dual union all
select 'Bob' parentname, 'Shara' name from dual union all
select 'Bob' parentname, 'Sandra' name from dual
)
,pet as (
select 'Tilda' childname, 'Dog' pet from dual union all
select 'Tilda' childname, 'Cat' pet from dual union all
select 'Shara' childname, 'Bird' pet from dual union all
select 'Shara' childname, 'Snake' pet from dual
)
select pa.name,ch.name,count(pe.pet)
from parent pa
inner join child ch on ch.parentname = pa.name
left join pet pe on pe.childname = ch.name
group by pa.name,ch.name

SQL querying the same table twice with criteria

I have 1 table
table contains something like:
ID, parent_item, Comp_item
1, 123, a
2, 123, b
3, 123, c
4, 456, a
5, 456, b
6, 456, d
7, 789, b
8, 789, c
9, 789, d
10, a, a
11, b, b
12, c, c
13, d, d
I need to return only the parent_items that have a Comp_item of a and b
so I should only get:
123
456
Here is a canonical way to do this:
SELECT parent_item
FROM yourTable
WHERE Comp_item IN ('a', 'b')
GROUP BY parent_item
HAVING COUNT(DISTINCT Comp_item) = 2
The idea here to aggregate by parent_item, restricting to only records having a Comp_item of a or b, then asserting that the distinct number of Comp_item values is 2.
Alternatively you could use INTERSECT:
select parent_item from my_table where comp_item = 'a'
intersect
select parent_item from my_table where comp_item = 'b';
If you have a parent item table, the most efficient method is possibly:
select p.*
from parent_items p
where exists (select 1 from t1 where t1.parent_id = p.parent_id and t1.comp_item = 'a') and
exists (select 1 from t1 where t1.parent_id = p.parent_id and t1.comp_item = 'b');
For optimal performance, you want an index on t1(parent_id, comp_item).
I should emphasize that I very much like the aggregation solution by Tim. I bring this up because performance was brought up in a comment. Both intersect and group by expend effort aggregating (in the first case to remove duplicates, in the second explicitly). An approach like this does not incur that cost -- assuming that a table with unique parent ids is available.