The problem:
I have a table that has family trees that has parents and child elements. What I need to return is the family tree for each child. I cant figure out how to do this with a cte or alike
Table:
ID
Name
Parent
1
Child1
2
2
parent1
3
3
Grandparent1
null
4
Child2
5
5
parent2
null
6
Child3
null
Expected results:
ID
Family
1
grandparent1, parent1
2
grandparent1
3
null
4
parent2
5
null
6
null
This is basically a recursive CTE. The tricky part is removing the current name from the family list.
The following approach uses the recursive CTE to generate all family members and then returns the current name. If the names can overlap significantly (such as "grandchild1" and "child1"), then it might need to be tweaked. But it works for your example:
with cte as (
select id, name, convert(varchar(max), ',' + name) as family, 1 as lev
from t
where parent is null
union all
select t.id, t.name, concat(cte.family, ',', t.name), lev + 1
from cte join
t
on t.parent = cte.id
)
select id, name,
stuff(nullif(replace(family, ',' + name, ''), ''), 1, 1, '') as familh
from (select cte.*, max(lev) over (partition by id) as max_lev
from cte
) cte
where lev = max_lev;
Here is a little db<>fiddle.
Thank you for taking the time out to read this post, all help is greatly appreciated.
I need help creating an SQL query that returns all ancestors and descendants of items that match or contain my WHERE query string for the description.
My table is like so:
ID Hierarchy Name
1 / Products
2 /1/ Cars
3 /1/1/ Red
4 /1/2/ Blue
5 /2/ Bike
6 /2/1/ Green
7 /2/2/ Red
I would like to search by description and all matches should be returned along with their descendants and ancestors.
e.g. Search Term = 'Red'
Result
ID Hierarchy Name
1 / Products
2 /1/ Cars
3 /1/1/ Red
5 /2/ Bike
7 /2/2/ Red
another example:
Search Term = 'bi'
Result (because bi is contained in the Bike string
ID Hierarchy Name
1 / Products
5 /2/ Bike
6 /2/1/ Green
7 /2/2/ Red
Many thanks,
Rob
********** EDIT 28-Mar-14 ***********
I'm almost there with the following query. However it only retrieves all ancestors and not descendants.
WITH Ancestors(Hierarchy, [Name], AncestorId) AS
(
SELECT
Hierarchy, [Name], Hierarchy.GetAncestor(1)
FROM
dbo.SpecProducts
WHERE
Name = 'Chrome' -- or whatever you need to select that node
UNION ALL
SELECT
ht.Hierarchy, ht.[Name], ht.Hierarchy.GetAncestor(1)
FROM
dbo.SpecProducts ht
INNER JOIN
Ancestors a ON ht.Hierarchy = a.AncestorId
)
SELECT DISTINCT *, Hierarchy.ToString() FROM Ancestors
****** EDIT 07-Mar-14 *************
#Yousseff DAOUI - In response to your answer, I'm having difficulty converting your code to work with my table. Below is my attempt to make your code work with my Table:
DECLARE #p_name NVARCHAR = 'Gr';
WITH Temp AS (SELECT * FROM SpecProducts) -- DB Table
,Temp_parents AS (
SELECT ID, Hierarchy, Name --, 0 _Level
FROM
Temp
WHERE
Name LIKE '%'+#p_name+'%'
UNION ALL
SELECT tab.ID, tab.Hierarchy, tab.Name --, p._Level+1
FROM
Temp tab
INNER JOIN
Temp_parents p
ON p.Hierarchy LIKE tab.Hierarchy+'_/')
,Temp_descendants AS (
SELECT ID, Hierarchy, Name --, 0 _Level
FROM
Temp
WHERE
Name LIKE '%'+#p_name+'%'
UNION ALL
SELECT tab.ID, tab.Hierarchy, tab.Name --, d._Level-1
FROM
Temp tab
INNER JOIN
Temp_descendants d
ON tab.Hierarchy LIKE d.Hierarchy+'_/')
SELECT *
FROM Temp_parents
UNION
SELECT *
FROM Temp_descendants
Notice how Line 3 attempts to pull data from my table named SpecProducts:
WITH Temp AS (SELECT * FROM SpecProducts) -- DB Table
However, MS SQL Server Management Studio keeps outputting the following error when I try to execute the code:
Msg 403, Level 16, State 1, Line 3
Invalid operator for data type. Operator equals add, type equals hierarchyid.
Do you know how I can get this to work?
Many thanks.
** 2nd Edit - 07-Mar-14 *
This seems a bit better, but the query is really slow (I think it's pulling more rows than it should be). Is it because HierarchyID '/' is being returned, thus is then finding all children of the root '/', which is everything?
DECLARE #p_name NVARCHAR = 'wallpaper';
WITH Temp AS (SELECT * FROM SpecProducts) -- DB Table
,Temp_parents AS (
SELECT ID, Hierarchy, Name --, 0 _Level
FROM
Temp
WHERE
Name LIKE '%'+#p_name+'%'
UNION ALL
SELECT tab.ID, tab.Hierarchy, tab.Name --, p._Level+1
FROM
Temp tab
INNER JOIN
Temp_parents p
ON p.Hierarchy.ToString() LIKE tab.Hierarchy.ToString()+'_/')
,Temp_descendants AS (
SELECT ID, Hierarchy, Name --, 0 _Level
FROM
Temp
WHERE
Name LIKE '%'+#p_name+'%'
UNION ALL
SELECT tab.ID, tab.Hierarchy, tab.Name --, d._Level-1
FROM
Temp tab
INNER JOIN
Temp_descendants d
ON tab.Hierarchy.ToString() LIKE d.Hierarchy.ToString()+'_/')
SELECT *
FROM Temp_parents
UNION
SELECT *
FROM Temp_descendants
***** 3rd EDIT - 07-Mar-14 *******
Ok, really sorry for the number of edits. But this now seems to work based on Yousseff's code.
DECLARE #p_name NVARCHAR(255) = 'natural';
WITH Temp_parents AS (
SELECT ID, Hierarchy, Name --, 0 _Level
FROM
SpecProducts
WHERE
Name LIKE '%'+#p_name+'%'
UNION ALL
SELECT tab.ID, tab.Hierarchy, tab.Name --, p._Level+1
FROM
SpecProducts tab
INNER JOIN
Temp_parents p
ON p.Hierarchy.ToString() LIKE tab.Hierarchy.ToString()+'_/')
,Temp_descendants AS (
SELECT ID, Hierarchy, Name --, 0 _Level
FROM
SpecProducts
WHERE
Name LIKE '%'+#p_name+'%'
UNION ALL
SELECT tab.ID, tab.Hierarchy, tab.Name --, d._Level-1
FROM
SpecProducts tab
INNER JOIN
Temp_descendants d
ON tab.Hierarchy.ToString() LIKE d.Hierarchy.ToString()+'_/')
SELECT *
FROM Temp_parents
UNION
SELECT *
FROM Temp_descendants
I've got a solution for you, i hope it will help you, Here's the code :
DECLARE #p_name NVARCHAR(255) = 'b'
IF OBJECT_ID('Temp') IS NOT NULL
DROP TABLE Temp
IF OBJECT_ID('Temp_parents') IS NOT NULL
DROP TABLE Temp_parents
IF OBJECT_ID('Temp_descendants') IS NOT NULL
DROP TABLE Temp_descendants
SELECT *
INTO Temp
FROM (
SELECT 1 AS ID, '/' AS Hierarchy, 'Products' Name UNION ALL
SELECT 2 AS ID, '/1/' AS Hierarchy, 'Cars' Name UNION ALL
SELECT 3 AS ID, '/1/1/' AS Hierarchy, 'Red' Name UNION ALL
SELECT 4 AS ID, '/1/2/' AS Hierarchy, 'Blue' Name UNION ALL
SELECT 5 AS ID, '/2/' AS Hierarchy, 'Bike' Name UNION ALL
SELECT 6 AS ID, '/2/1/' AS Hierarchy, 'Green' Name UNION ALL
SELECT 7 AS ID, '/2/2/' AS Hierarchy, 'Red' Name) AS T
DECLARE #v_name_1 NVARCHAR(255)= #p_name;
WITH parents AS (
SELECT ID, Hierarchy, Name, 0 _Level
FROM
Temp
WHERE
Name LIKE '%'+#v_name_1+'%'
UNION ALL
SELECT tab.ID, tab.Hierarchy, tab.Name, p._Level+1
FROM
Temp tab
INNER JOIN
parents p
ON p.Hierarchy LIKE tab.Hierarchy+'_/')
select ID, Hierarchy, Name
INTO Temp_parents
from parents
DECLARE #v_name_2 NVARCHAR(255)= #p_name;
WITH descendants AS (
SELECT ID, Hierarchy, Name, 0 _Level
FROM
Temp
WHERE
Name LIKE '%'+#v_name_2+'%'
UNION ALL
SELECT tab.ID, tab.Hierarchy, tab.Name, d._Level-1
FROM
Temp tab
INNER JOIN
descendants d
ON tab.Hierarchy LIKE d.Hierarchy+'_/')
SELECT ID,Hierarchy ,Name
INTO Temp_descendants
FROM
descendants
WHERE
ID IS NOT NULL
SELECT *
FROM Temp_parents
UNION
SELECT *
FROM Temp_descendants
IF OBJECT_ID('Temp') IS NOT NULL
DROP TABLE Temp
IF OBJECT_ID('Temp_parents') IS NOT NULL
DROP TABLE Temp_parents
IF OBJECT_ID('Temp_descendants') IS NOT NULL
DROP TABLE Temp_descendants
Good Luck :)
Here's an other way of writing the code based on the first to make the code look easier :)
DECLARE #p_name NVARCHAR(255) = 'Gr';
WITH Temp AS (
SELECT 1 AS ID, '/' AS Hierarchy, 'Products' Name UNION ALL
SELECT 2 AS ID, '/1/' AS Hierarchy, 'Cars' Name UNION ALL
SELECT 3 AS ID, '/1/1/' AS Hierarchy, 'Red' Name UNION ALL
SELECT 4 AS ID, '/1/2/' AS Hierarchy, 'Blue' Name UNION ALL
SELECT 5 AS ID, '/2/' AS Hierarchy, 'Bike' Name UNION ALL
SELECT 6 AS ID, '/2/1/' AS Hierarchy, 'Green' Name UNION ALL
SELECT 7 AS ID, '/2/2/' AS Hierarchy, 'Red' Name)
,Temp_parents AS (
SELECT ID, Hierarchy, Name --, 0 _Level
FROM
Temp
WHERE
Name LIKE '%'+#p_name+'%'
UNION ALL
SELECT tab.ID, tab.Hierarchy, tab.Name --, p._Level+1
FROM
Temp tab
INNER JOIN
Temp_parents p
ON p.Hierarchy LIKE tab.Hierarchy+'_/')
,Temp_descendants AS (
SELECT ID, Hierarchy, Name --, 0 _Level
FROM
Temp
WHERE
Name LIKE '%'+#p_name+'%'
UNION ALL
SELECT tab.ID, tab.Hierarchy, tab.Name --, d._Level-1
FROM
Temp tab
INNER JOIN
Temp_descendants d
ON tab.Hierarchy LIKE d.Hierarchy+'_/')
SELECT *
FROM Temp_parents
UNION
SELECT *
FROM Temp_descendants
Good Luck
EDIT 07-Mar-14 - For this to work with my table I had to amend it to the following - This now appears work well **
DECLARE #p_name NVARCHAR(255) = 'natural';
WITH Temp_parents AS (
SELECT ID, Hierarchy, Name --, 0 _Level
FROM
SpecProducts
WHERE
Name LIKE '%'+#p_name+'%'
UNION ALL
SELECT tab.ID, tab.Hierarchy, tab.Name --, p._Level+1
FROM
SpecProducts tab
INNER JOIN
Temp_parents p
ON p.Hierarchy.ToString() LIKE tab.Hierarchy.ToString()+'_/')
,Temp_descendants AS (
SELECT ID, Hierarchy, Name --, 0 _Level
FROM
SpecProducts
WHERE
Name LIKE '%'+#p_name+'%'
UNION ALL
SELECT tab.ID, tab.Hierarchy, tab.Name --, d._Level-1
FROM
SpecProducts tab
INNER JOIN
Temp_descendants d
ON tab.Hierarchy.ToString() LIKE d.Hierarchy.ToString()+'_/')
SELECT *
FROM Temp_parents
UNION
SELECT *
FROM Temp_descendants
maybe like this:
select id, HierarchyID, Description from (
select b.id, b.HierarchyID, b.Description,
case
when (a.id = b.id) then 1
when (marker = 1 and num_len > 1 and LEN(replace(b.HierarchyID,'/','')) < num_len and numbers > replace(b.HierarchyID,'/','')) then 1
when (marker = 1 and num_len = 1 and LEN(replace(b.HierarchyID,'/','')) > num_len and numbers < replace(b.HierarchyID,'/','')) then 1
else 0 end as marker from (
select id, SUBSTRING(HierarchyID,2,1) as first_num, 1 as marker, replace(HierarchyID,'/','') as numbers, LEN(replace(HierarchyID,'/','')) as num_len
from my_tab where Description like '%bi%' or HierarchyID = '/') a right join my_tab b on first_num = SUBSTRING(b.HierarchyID,2,1)) c where marker = 1;
sorry for my english!
select id, the first number you find in HierarchyID ("/2/1/" is 2), a "marker" for the values you found and you want to Keep (marker = 1), the complete number you can find in HierarchyID ("/2/1/" is 21) and the length of the complete number you found in HierarchyID from my_tab table (a) with the search term of your choice.
with (1.) we make a right join of the my_tab table (b) to get all the other values.
first case when: if id from (a)is equal with the id of (b) we take the marker 1 (because thats the values we got with the search term). second case when: if marker is 1 and the length of (a) > 1 (that means we have a value like "/2/1/") and the length of (b) < the length of (a) (that means "/2/2/" has marker = 1 and a length of 2; "/2/1/" has the same length, but we dont want him; we want "/2/") and the complete number you can find in HierarchyID (a) is greater than the complete number you can find in HierarchyID (b) (that means we will get the "/2/" value) we set the marker to 1. third case when: similar to the "second case when". Here we have a look at the values with marker = 1 and a length of 1 ("/2/") to mark all the values like "/2/1/" and "/2/2/" wich we want.
now you have all the values you want with a marker = 1.
I found this nice example of SQL recursion with a CTE, but fail to apply it to my table:
http://walkingoncoals.blogspot.de/2009/12/fun-with-recursive-sql-part-1.html
I have the following table (ObjectStates):
ID Title ParentID
1 Draft null
2 Green null
3 Red null
4 Foo 1
5 Bar 4
I am trying to create a function which returns the "main" state when queried. Example:
GetMainState(5)
-- Shall return 1
GetMainState(4)
-- Shall return 1
GetMainState(2)
-- Shall return 2
I have so far:
CREATE FUNCTION [dbo].[GetMainObjectState] (#ObjectStateID INT)
RETURNS TABLE
AS
RETURN
(
WITH StateRecurcsion(ID, ParentID, Level) AS
(
SELECT ID, ParentID, 0
FROM ObjectStates
WHERE ID = #ObjectStateID
UNION ALL
SELECT uOS.ID, uOS.ParentID, sOS.Level+1
FROM ObjectStates uOS, StateRecurcsion sOS
WHERE uOS.ParentID= sOS.ID
)
SELECT os.ID, os.Title, sos.Level
FROM ObjectStates os, StateRecurcsion sos
WHERE os.ID = sos.ID
)
GO
I tried to create the function just as in the tutorial shown above, but somehow I'm not getting the correct results.
You could create a CTE containing a "root" value and then query it within your function e.g.:
;WITH CTEHierarchy
AS (
SELECT
ID
,0 AS LEVEL
,ID AS root
FROM ObjectStates
WHERE ParentID IS NULL
UNION ALL
SELECT
ObjectStates.ID
,LEVEL + 1 AS LEVEL
,[root]
FROM ObjectStates
INNER JOIN CTEHierarchy uh ON uh.id = ObjectStates.ParentID
)
SELECT [root]
FROM CTEHierarchy
WHERE ID = #ObjectStateID
I am looking to return a query that shows exactly like this:
Root 1
--> Child 2
----->Child 3
Root 2
--> Child 4
---->Child 5
So the query should return Root 1 as One row, ---> Child 2 as another row. Assume n Levels, and "--->" format is placed for each child. Level is higher then "---->" increases.
My Table definition is
[NodeId, ParentId, Name, Level]
On SQL Server 2008 and above, you can use hierarchyId datatype to quickly achieve the desired sorting. You can use REPLICATE() to get the dashes.
;with cte as (
select NodeId, ParentId, Name, 0 Level, '/' + cast(NodeId as varchar(max)) + '/' Hier
from tbl1
where ParentId is null
union all
select t.NodeId, t.ParentId, t.Name, Level+1, Hier + cast(t.NodeId as varchar(max)) + '/'
from tbl1 t
join cte c on t.ParentId = c.NodeId
)
select case when level=0
then ''
else replicate('-',level*2) + '>' end + Name
from cte
order by cast(Hier as hierarchyid);
SQL Fiddle
On earlier SQL Server 2005, you can emulate the hierarchyId sorting using zero-padded strings:
;with cte as (
select NodeId, ParentId, Name, 0 Level, right(replicate('0',10)+cast(NodeId as varchar(max)),11) Hier
from tbl1
where ParentId is null
union all
select t.NodeId, t.ParentId, t.Name, Level+1, Hier + right(replicate('0',10)+cast(t.NodeId as varchar(max)),11)
from tbl1 t
join cte c on t.ParentId = c.NodeId
)
select case when level=0
then ''
else replicate('-',level*2) + '>' end + Name
from cte
order by Hier;
I have a table like this
childid parentid
------------------------
1 0
2 1
3 2
4 2
5 3
6 4
7 0
8 7
9 8
10 1
If I give a childid as 5, the parentid will be 1(output)
If I give a childid as 9, the parentid will be 7.(output)
i.e. the root parentid is 0 and the query should stop there.
How to solve such a query?
Please help.
I think you should rename your child_id to node, your parent_id to child_of. Your column naming is a bit confusing
create table stack_overflow
(
node int, child_of int
);
insert into stack_overflow(node, child_of) values
(1,0),
(2,1),
(3,2),
(4,2),
(5,3),
(6,4),
(7,0),
(8,7),
(9,8),
(10,1);
This works on any CTE-capable RDBMS:
with find_parent(parent, child_of, recentness) as
(
select node, child_of, 0
from stack_overflow
where node = 9
union all
select i.node, i.child_of, fp.recentness + 1
from stack_overflow i
join find_parent fp on i.node = fp.child_of
)
select top 1 parent from find_parent
order by recentness desc
Output:
parent
7
[EDIT: more flexible and future-proof]:
with find_parent(node_group, parent, child_of, recentness) as
(
select node, node, child_of, 0
from stack_overflow
where node in (5,9)
union all
select fp.node_group, i.node, i.child_of, fp.recentness + 1
from stack_overflow i
join find_parent fp on i.node = fp.child_of
)
select q.node_group as to_find, parent as found
from find_parent q
join
(
select node_group, max(recentness) as answer
from find_parent
group by node_group
) as ans on q.node_group = ans.node_group and q.recentness = ans.answer
order by to_find
Output:
to_find found
5 1
9 7
If you're using Postgres, the above code could be shortened to:
with recursive find_parent(node_group, parent, child_of, recentness) as
(
select node, node, child_of, 0
from stack_overflow
where node in (5,9)
union all
select fp.node_group, i.node, i.child_of, fp.recentness + 1
from stack_overflow i
join find_parent fp on i.node = fp.child_of
)
select distinct on (node_group) node_group as to_find, parent as found
from find_parent
order by to_find, recentness desc
DISTINCT ON rocks! :-)
If ALL you want is the root ParentID, you can use this recursive function:
CREATE FUNCTION test_func
(
#ParentID int
)
RETURNS int
AS
BEGIN
DECLARE #result int;
DECLARE #childID int;
SET #childID = (SELECT ChildID FROM YourTable WHERE ParentID = #ParentID)
IF (#childID = 0)
SET #result = #ParentID
ELSE
SET #result = dbo.test_func(#childID)
RETURN #result
END
GO
then in your main query:
SELECT dbo.test_func(5)
Passing in 5 returns 1, 9 returns 7 based on your provided data. If you need every ParentID that is up that chain, you should probably use a CTE.
I think you want a recursive query, you should use Common Table Expressions. I will give you a link with an example very similar that the one you're using.
I think here is the solution. It helped me some months ago.
A simple of example of getting the parent ID matching a given child ID is:
select parentid
from MyTable
where childid = 5
However, for the data above, this will return no records.