Use a CTE to traverse to 2nd level in tree - sql

I'm trying to use a CTE to traverse a tree in SQL Server. Ideally what I would like as output is a table which shows for each node in the tree the corresponding node that is second from the top in the tree.
I have some basic code to traverse the tree from a given node, but how can I modify it so it produces the desired output ?
DECLARE #temp TABLE
(
Id INT
, Name VARCHAR(50)
, Parent INT
)
INSERT #temp
SELECT 1,' Great GrandFather Thomas Bishop', null UNION ALL
SELECT 2,'Grand Mom Elian Thomas Wilson' , 1 UNION ALL
SELECT 3, 'Dad James Wilson',2 UNION ALL
SELECT 4, 'Uncle Michael Wilson', 2 UNION ALL
SELECT 5, 'Aunt Nancy Manor', 2 UNION ALL
SELECT 6, 'Grand Uncle Michael Bishop', 1 UNION ALL
SELECT 7, 'Brother David James Wilson',3 UNION ALL
SELECT 8, 'Sister Michelle Clark', 3 UNION ALL
SELECT 9, 'Brother Robert James Wilson', 3 UNION ALL
SELECT 10, 'Me Steve James Wilson', 3
;WITH cte AS
(
SELECT Id, Name, Parent, 1 as Depth
FROM #temp
WHERE Id = 8
UNION ALL
SELECT t2.*, Depth + 1 as 'Depth'
FROM cte t
JOIN #temp t2 ON t.Parent = t2.Id
)
SELECT *
, MAX(Depth) OVER() - Depth + 1 AS InverseDepth
FROM cte
As output I would like something like
Id Name depth2_id depth2_name
8 Sister Michelle .. 2 Grand Mom Elian ....
7 Brother David .. 2 Grand Mom Elian ....
4 Uncle Michael .. 2 Grand Mom Elian ...
Thanks for any tips or pointers.

a bit hard to get what your goal, but you can use smth like this:
;with cte AS
(
select
t.Id, t.Name, t.Parent, 1 as Depth,
null as Depth2Parent
from #temp as t
where t.Parent is null
union all
select
t.Id, t.Name, t.Parent, c.Depth + 1 as 'Depth',
isnull(c.Depth2Parent, case when c.Depth = 1 then t.Id end) as Depth2Parent
from cte as c
inner join #temp as t on t.Parent = c.Id
)
select *
from cte
sql fiddle demo

Related

Find oldest ancestor for each entry in a SQL table

I have a SQL table where the rows in the table have a parent-child relationship with each other. I would like to write a recursive SQL query to find the oldest ancestor for each of these rows (or the row itself if it has no parent). So in other words, if my table looks like this:
Child
Parent
1
null
2
1
3
2
4
null
5
4
6
null
7
null
8
3
9
5
Then my final output should be:
Child
OldestAncestor
1
1
2
1
3
1
4
4
5
4
6
6
7
7
8
1
9
4
Can this be done? I know how to use recursive SQL to find an individual parent-child relationship, I'm just not sure if that can be taken a step further to find the parent's parent, etc.
Update - I was able to figure out a solution based on this article:https://www.webcodeexpert.com/2020/04/sql-server-cte-recursive-query-to-get.html
WITH parentChild(Child, Parent) AS
(
SELECT 1, null UNION ALL
SELECT 2, 1 UNION ALL
SELECT 3, 2 UNION ALL
SELECT 4, null UNION ALL
SELECT 5, 4 UNION ALL
SELECT 6, null UNION ALL
SELECT 7, null UNION ALL
SELECT 8, 3 UNION ALL
SELECT 9, 5
),
MyCTE AS
(
SELECT pc.Child, pc.Child AS 'OldestAncestor'
FROM parentChild pc
WHERE pc.Parent IS NULL
UNION ALL
SELECT pc2.Child, m.OldestAncestor AS 'OldestAncestor'
FROM parentChild pc2
INNER JOIN MyCTE m ON pc2.Parent = m.Child
)
SELECT * FROM MyCTE ORDER BY Child
In Oracle, you can use:
SELECT child,
CONNECT_BY_ROOT child AS oldest_ancestor
FROM table_name
START WITH parent IS NULL
CONNECT BY parent = PRIOR child
ORDER BY child;
or a recursive sub-query factoring clause (WITH clause):
WITH rsqfc (child, oldest_ancestor) AS (
SELECT child, child
FROM table_name
WHERE parent IS NULL
UNION ALL
SELECT t.child, r.oldest_ancestor
FROM rsqfc r
INNER JOIN table_name t
ON (t.parent = r.child)
)
SELECT *
FROM rsqfc
ORDER BY child;
Which, for the sample data:
CREATE TABLE table_name (Child, Parent) 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, null FROM DUAL UNION ALL
SELECT 7, null FROM DUAL UNION ALL
SELECT 8, 3 FROM DUAL UNION ALL
SELECT 9, 5 FROM DUAL;
Both output:
CHILD
OLDEST_ANCESTOR
1
1
2
1
3
1
4
4
5
4
6
6
7
7
8
1
9
4
db<>fiddle here
You can use a recursive CTE to find the paths from the parents to the leaf nodes, and then join the CTE back onto the original table:
with recursive cte(n1, n2) as (
select child, child from tbl where parent is null
union all
select c.n1, t.child from cte c join tbl t on c.n2 = t.parent
)
select t1.child, c.n1 from tbl t1 join cte c on t1.child = c.n2;

Get nested children from parent's ID with SQL Server

My table is as follows:
ID Name Parent ID
1 Joe -
2 James -
3 Mike 1
4 Lewis 3
5 Anne 2
6 Lucy 4
I'd like to get the ID of the parent and all its children. For example, if I do:
Select Name from Table where ID = 1 (and nested children)
The desired output would be:
Joe
Mike
Lewis
Lucy
You can use a recursive CTE:
with cte as (
select name, id
from t
where id = 1
union all
select t.name, t.id
from cte join
t
on t.parent_id = cte.id
)
select name
from cte;
WITH CTE_Table AS (
SELECT ID, Name, Parent
FROM MyTable
UNION ALL
SELECT MyTable.ID, MyTable.Name, MyTable.Parent
FROM CTE_Table INNER JOIN
MyTable ON CTE_Table.ID = MyTable.Parent
)
SELECT Name
FROM CTE_Table
WHERE (Parent = 1)

Display titles which are missing association values partially or completely

I am having problem writing SQL query for the following scenario. I need someone's help to write the query.
I have the following tables 7 tables:
1) Titles
ID Title Author
-------------------------------------------------------------------------
1 The Hidden Language of Computer Hardware and Software Charles Petzold
2 Paths, Dangers, Strategies Nick Bostrom
3 The Smart Girl's Guide to Privacy Violet Blue
4 Introduction to Algorithms Thomas H. Cormen
5 Machine Learning in Action Peter Harrington
...
2) Themes
ID Name
------------------------------------------
1 Science Fiction
2 Biography
3 Painting
...
3) Subjects
ID Name
-----------------------------------
1 Science
2 Technology
3 Music
4 Geography
...
4) Grades
ID Name
------------------------------------
1 Grade 1
2 Grade 2
3 Grade 3
4 Grade 4
5 Grade 5
...
5) TitleThemeAssociation
TitleID ThemeID
------------------------------------------
1 1
1 3
4 2
4 3
...
6) TitleSubjectAssociaton
TitleID SubjectID
---------------------------------
1 1
1 3
2 1
2 3
4 1
4 2
...
7) TitleGradeAssociaton
TitleID GradeID
1 1
1 2
1 3
2 1
2 2
...
I need to write a query to display only titles which are missing any of three values (Themes, Subjects and Grades) or not assigned values completely. I should not display the title if all three values (Themes, Subjects, Grades) are assigned. In the above data set since TitleID 1 has all three values it should not be present in the list. TitleID 2 has only Subjects and Grades assigned but not Themes so it should be displayed in the output. While listing the titles if a title has multiple values then they should be contacted with comma (,) separator.
So the final output of the above data set should be as below:
Output:
Title ID Title Theme Subject Grade
-------------------------------------------------------------------------------------------
2 Paths, Dangers, Strategies - Science, Music Grade 1, Grade 2
3 The Smart Girl's Guide to Privacy - - -
4 Introduction to Algorithms Biography, Painting Science, Technology -
5 Machine Learning in Action - - -
There are essentially two questions you're asking. The first being how to filter when either a Theme, Subject or Grade is missing. And the other is asking how to concat these items into a comma separated list.
The following query should be what you're looking for:
Select Distinct
T.Id As [Title ID],
T.Title,
H.Theme,
S.Subject,
G.Grade
From Titles T
Outer Apply
(
Select Stuff(( Select ', ' + Name
From Themes H
Join TitleThemeAssociation TH On H.Id = TH.ThemeId
Where TH.TitleId = T.Id
For Xml Path('')), 1, 2, '') As Theme
From Themes
) H
Outer Apply
(
Select Stuff(( Select ', ' + Name
From Subjects S
Join TitleSubjectAssociaton TS On S.Id = TS.SubjectId
Where TS.TitleId = T.Id
For Xml Path('')), 1, 2, '') As Subject
From Subjects
) S
Outer Apply
(
Select Stuff(( Select ', ' + Name
From Grades G
Join TitleGradeAssociaton TG On G.Id = TG.GradeId
Where TG.TitleId = T.Id
For Xml Path('')), 1, 2, '') As Grade
From Grades
) G
Where H.Theme Is Null
Or S.Subject Is Null
Or G.Grade Is Null
Hope this Helps.
;WITH cte_Titles (ID,Title,Author) AS
(
SELECT 1,'The Hidden Language of Computer Hardware and Software','Charles Petzold' UNION ALL
SELECT 2,'Paths, Dangers, Strategies','Nick Bostrom' UNION ALL
SELECT 3,'The Smart Girls Guide to Privacy','Violet Blue' UNION ALL
SELECT 4,'Introduction to Algorithms','Thomas H. Cormen' UNION ALL
SELECT 5,'Machine Learning in Action','Peter Harrington'
),cte_Themes(ID,Name) AS
(
SELECT 1,'Science Fiction' UNION ALL
SELECT 2,'Biography' UNION ALL
SELECT 3,'Painting'
),cte_Subjects(ID,Name) AS
(
SELECT 1,'Science' UNION ALL
SELECT 2,'Technology' UNION ALL
SELECT 3,'Music' UNION ALL
SELECT 4,'Geography'
),cte_Grades(ID,Name) AS
(
SELECT 1,'Grade 1' UNION ALL
SELECT 2,'Grade 2' UNION ALL
SELECT 3,'Grade 3' UNION ALL
SELECT 4,'Grade 4' UNION ALL
SELECT 5,'Grade 5'
),cte_TitleThemeAssociation(TitleID,ThemeID) AS
(
SELECT 1,1 UNION ALL
SELECT 1,3 UNION ALL
SELECT 4,2 UNION ALL
SELECT 4,3
),cte_TitleSubjectAssociaton(TitleID,SubjectID) AS
(
SELECT 1, 1 UNION ALL
SELECT 1, 3 UNION ALL
SELECT 2, 1 UNION ALL
SELECT 2, 3 UNION ALL
SELECT 4, 1 UNION ALL
SELECT 4, 2
),cte_TitleGradeAssociaton(TitleID,GradeID) AS
(
SELECT 1, 1 UNION ALL
SELECT 1, 2 UNION ALL
SELECT 1, 3 UNION ALL
SELECT 2, 1 UNION ALL
SELECT 2, 2
)
,cte_ResultSet AS
(
SELECT DISTINCT t.ID AS TitleID,
t.Title,
th.NAME AS Theme,
s.NAME AS Subject,
g.NAME AS Grade
FROM cte_Titles t
LEFT JOIN cte_TitleThemeAssociation tta
ON t.ID = tta.TitleID
LEFT JOIN cte_Themes th
ON tta.ThemeID = th.ID
LEFT JOIN cte_TitleSubjectAssociaton tsa
ON tsa.TitleID = t.ID
LEFT JOIN cte_Subjects s
ON tsa.SubjectID = s.ID
LEFT JOIN cte_TitleGradeAssociaton tga
ON tga.TitleID = t.ID
LEFT JOIN cte_Grades g
ON g.ID = tga.GradeID
)
SELECT DISTINCT Title
, STUFF((SELECT DISTINCT ',' + SUB.Theme AS [text()]
FROM cte_ResultSet SUB
WHERE SUB.TitleID = CAT.TitleID
FOR XML PATH('')
), 1, 1, '' ) AS Theme
, STUFF((SELECT DISTINCT ',' + SUB.Subject AS [text()]
FROM cte_ResultSet SUB
WHERE SUB.TitleID = CAT.TitleID
FOR XML PATH('')
), 1, 1, '' ) AS Subject
, STUFF((SELECT DISTINCT ',' + SUB.Grade AS [text()]
FROM cte_ResultSet SUB
WHERE SUB.TitleID = CAT.TitleID
FOR XML PATH('')
), 1, 1, '' ) AS Grade
FROM cte_ResultSet CAT

SQL Database Parent/Child recursion

Here is my table:
parent_id | child_id
--------------
1 | 2
1 | 3
1 | 4
2 | 5
2 | 6
5 | 8
8 | 9
9 | 5
I need to get all of the items under parent 2. I've found a few things similar to this, but but couldn't figure out how to make it work for my case. I keep getting maximum recursion limit reached. Here's what I have:
WITH CTE AS
(
SELECT gt.[child_id]
FROM [CHSPortal].[dbo].[company_adgroupstoadgroups] gt
WHERE gt.parent_id='2'
UNION ALL
SELECT g.[child_id]
FROM [CHSPortal].[dbo].[company_adgroupstoadgroups] g
INNER JOIN CTE g2 on g.parent_id=g2.child_id
)
select distinct child_id from CTE
The desired result is going to be: 2,3,4,5,6,8,9.
What modification do I need to make to get a list of all the items under child 2. I would also prefer 2 (the parent node) to be in the list. Any help would be appreciated.
First of all, there is a loop in your example (5|8, 8|9, 9|5), that is why you reach the maximum recursion limit.
Regarding the filtering question,below you can find an example for filtering by root node:
;WITH MTree (parent_id, child_id, LEVEL) AS (
SELECT t.parent_id , t.child_id, 0 AS LEVEL
FROM table_1 t
WHERE child_id = 2 --here you can filter the root node
UNION ALL
SELECT m.parent_id , m.child_id, LEVEL + 1
FROM Table_1 m
INNER JOIN MTree t ON t.child_id = m.parent_id
)
SELECT * FROM Mtree;
Not sure what's wrong with your query, aside from not relating to the sample data you provided, but this works just fine:
;WITH src AS (SELECT 1 AS parent_id, 2 AS child_id
UNION SELECT 1, 3
UNION SELECT 1, 4
UNION SELECT 2, 5
UNION SELECT 2, 6
UNION SELECT 5, 8
UNION SELECT 8, 9
UNION SELECT 9, 5)
,cte AS (SELECT *
FROM src
WHERE child_id = 2
UNION ALL
SELECT a.*
FROM src a
JOIN cte b
ON a.parent_id = b.child_id
)
SELECT TOP 100 *
FROM cte
--Limited to top 100 because of infinite recursion problem with sample data.

Binary "OR" operation on a SQL column

I have a query that returns weekdays
Select PERSON_NAME, PERSON_DAY from PERSON_DAYS WHERE PERSON_ID = #myId
say I obtain
John 1 (mo)
John 3 (mo tu)
John 8 (th)
I need to obtain for John all the days when is busy. How do I a logical OR on the PERSON_DAY column in this query?
the result should be 11 (mo tu th)
well here my best so far
;with PowersOf2
as
(
select 1 as Number
union all
select A.Number * 2 from PowersOf2 as A where A.Number < 64
)
select P.PERSON_NAME, sum(distinct P.PERSON_DAY & PowersOf2.Number)
from PERSON_DAYS as P
left outer join PowersOf2 on PowersOf2.Number <= P.PERSON_DAY
where P.PERSON_ID = #myId
group by P.PERSON_NAME
SQL FIDDLE EXAMPLE
If I understand you correctly, you can use a combination of bitwise operator and and aggregate function sum to do what you want.
Example:
with person_days as (
select 'John' as person_name, 1 as weekday --mo
union select 'John', 3 -- mo, tu
union select 'John', 8 -- th
union select 'Jane', 1 -- mo
union select 'Jane', 9 -- mo, th
union select 'Jane', 40 -- th, sa
),
Bits AS (
SELECT 1 AS BitMask --mo
UNION ALL SELECT 2 --tu
UNION ALL SELECT 4 --we
UNION ALL SELECT 8 --th
UNION ALL SELECT 16 --fr
UNION ALL SELECT 32 --sa
UNION ALL SELECT 64 --su
UNION ALL SELECT 128
)
, person_single_days as (
select distinct person_name, weekday & bits.BitMask single_weekday
from person_days
inner join bits on person_days.weekday & bits.BitMask > 0
)
select person_name, sum(single_weekday) weekdays
from person_single_days
group by person_name;
result:
person_name weekdays
----------- -----------
Jane 41
John 11
"inspired" by Roman's CTE: (note that the first CTE just generates demo data)
with p as
(
select 'John' as PERSON_NAME, 1 as PERSON_DAY
union
select 'John', 3
union
select 'John', 8
union
select 'Jane', 2
union
select 'Jane', 4
),
cte as
(
select PERSON_NAME, PERSON_DAY from p
union all
select cte2.PERSON_NAME, p.PERSON_DAY | cte2.PERSON_DAY
from p
inner join cte as cte2 on p.PERSON_NAME = cte2.PERSON_NAME
where p.PERSON_DAY & cte2.PERSON_DAY = 0
)
select PERSON_NAME, MAX(PERSON_DAY) from cte
group by PERSON_NAME
I think what you are looking for is a custom aggregate that does an OR. You can write that using SQL CLR in .NET. This is probably the cleanest solution. It will be reusable, too.
Alternatively, you could use cursor-based loops to calculate the result.
You could also (mis)use CTE's for this purpose.