Description:
There is table wich consist of two columns (ParentId and ChildId) that displays hierarchy of some entities. Each Id could be presented in ParentId column only one time. That means that each entity have only one child entity.
Problem: I need to check whether entity(its Id) is in descendants list of parent entity.
DECLARE #parentId INT
DECLARE #childId INT
DECLARE #targetChildId INT
SET #targetChildId=<put id of a child you want to find>
SET #parentId=<put id of a parent you are looking child for>
SET #childId=0
WHILE (#childId<>#targetChildId)
BEGIN
IF(EXISTS(SELECT ChildId FROM Hierarchies WHERE ParentId=#parentId))
BEGIN
SET #childId=(SELECT ChildId FROM Hierarchies WHERE ParentId=#parentId)
SET #parentId=#childId
END
ELSE
BEGIN
SET #childId=0
BREAK
END
END
PRINT #childId
It returns 0 if target child not found in target parent.
Sample data:
CREATE TABLE [dbo].[EntityHierarchy]
(
[EntityId] INT,
[ChildEntityId] INT
)
INSERT [dbo].[EntityHierarchy]
VALUES (1, 2),
(2, 3),
(3, 4),
(4, 1) -- Cycle
Find circular relationships:
DECLARE #SearchEntityId INT = 1
;WITH [cteRursive] AS
(
SELECT 1 AS [ROW_NUMBER],
[ChildEntityId] AS [EntityId]
FROM [dbo].[EntityHierarchy]
WHERE [EntityId] = #SearchEntityId
UNION ALL
SELECT r.[ROW_NUMBER] + 1,
h.[ChildEntityId]
FROM [cteRursive] r
INNER JOIN [dbo].[EntityHierarchy] h
ON r.[EntityId] = h.[EntityId]
WHERE h.[ChildEntityId] <> #SearchEntityId
)
SELECT h.*
FROM [cteRursive] r
INNER JOIN [dbo].[EntityHierarchy] h
ON r.[EntityId] = h.[EntityId]
WHERE r.[ROW_NUMBER] = (SELECT MAX([ROW_NUMBER]) FROM [cteRursive])
I'm using a recursive CTE to list the descendants. The child of the last descendant either creates a cycle or not.
Related
Say I have the following hierarchical representation in my database:
A
|_B_C
|_D
then I want to get the child nodes from A (or B).
and vice versa, I want to get the parent from a given child node?
<>
CREATE TABLE tbl (
Node HierarchyID PRIMARY KEY CLUSTERED,
NodeLevel AS Node.GetLevel(),
ID INT UNIQUE NOT NULL,
Name VARCHAR(50) NOT NULL
)
inserting the root:
INSERT INTO tbl (Node, ID, Name)
VALUES (HierarchyId::GetRoot(), 1, 'A')
child B
DECLARE #parent HierarchyId = HierarchyId::GetRoot()
INSERT INTO tbl (Node,ID,Name) VALUES (#parent.GetDescendant(NULL,NULL),2,'B')
select #child = node from tbl where id = 2;
-- get immediate ancestor
select * from dbo.tbl where Node = #child.GetAncestor(1);
-- get immediate children
select
*
from
dbo.tbl
where
Node.IsDescendantOf(#parent) = 1 and
Node.GetLevel() = #parent.GetLevel() + 1;
I'm storing nested lists in my database using the adjacency list model. Each list might have 50-150 nodes, so we'll call it 100 nodes on average. The situation has come up where users want to clone a list (i.e., use an existing list as a template from which to create a new list). This use case could potentially save them a lot of time when new lists differ only slightly from existing lists.
Here's an abbreviated version of the table schema I'm using:
CREATE TABLE Nodes (
NodeId int IDENTITY(1,1) NOT NULL,
ParentId int NULL,
ListId int NOT NULL,
NodeText varchar(255) NOT NULL
)
My original thought was to use an INSERT ... SELECT to copy all the nodes in one shot, but that leaves the new records referencing the old ParentId values.
I've got a solution that's working (in application code, not SQL), but it seems suboptimal due to the number of queries required. Here's the algorithm:
Select all records belonging to old list.
Iterate over rows and add to new list by inserting with different ListId.
Select ##IDENTITY from each insert and store it alongside data for current row.
Iterate over rows again and update Nodes table, setting ParentId to new ID (from previous step) where ParentId is equal to old ID and ListId is equal to new list ID.
Like I said, that works fine, but it requires 300+ queries to clone a single list containing 100 nodes. Is there a more efficient way to achieve the same thing?
Try this. The following solution is a zero loop, zero temp table one.
SQLFiddle
DECLARE #CurrentID int = IDENT_CURRENT('Nodes'),
#OldListId int = 1,
#NewListId int;
SELECT #NewListId = ISNULL(MAX(ListId) ,0)+1 FROM Nodes
SET IDENTITY_INSERT Nodes ON
;WITH NewNode as (
SELECT ROW_NUMBER() OVER(ORDER BY NodeId)+ #CurrentID as NewNodeId, *
FROM Nodes WHERE ListId= #OldListId
)
INSERT INTO Nodes(NodeId,ParentId,ListId,NodeText)
SELECT N1.NewNodeId ,N2.NewNodeId , #NewListId, N1.NodeText FROM NewNode N1 LEFT OUTER JOIN NewNode N2 ON
N1.ParentId = N2.NodeId
--SELECT N1.* , N2.NewNodeId as NewParentId FROM NewNode N1 LEFT OUTER JOIN NewNode N2 ON
--N1.ParentId = N2.NodeId
SET IDENTITY_INSERT Nodes OFF
The above solution generates the tree and then inserts to the table. Please be aware to use appropriate transactions and locking mechanism to ensure the data is consistent
With suitable transaction wrapping and isolation, the following may meet your needs:
-- Set up some sample data.
declare #Nodes as Table ( NodeId Int Identity, ParentId Int Null, ListId Int, NodeText VarChar(255) );
insert into #Nodes ( ParentId, ListId, NodeText ) values
( NULL, 1, '1' ),
( 1, 1, '1.1' ), ( 1, 1, '1.2' ),
( NULL, 2, '2' ),
( 4, 2, '2.1' ), ( 5, 2, '2.1.1' );
select * from #Nodes;
declare #TemplateListId as Int = 2;
-- Assuming that the clone is a new List. This is not important to what follows.
declare #ListId as Int = Coalesce( ( select Max( ListId ) from #Nodes ), 0 ) + 1;
-- Copy the template rows into the table and generate a mapping from old to new NodeIds.
declare #Fixups as Table ( OldNodeId Int, NewNodeId Int );
with Template as (
select NodeId, ParentId, ListId, NodeText
from #Nodes
where ListId = #TemplateListId )
merge into #Nodes as Nodes
using Template as T
on 42 < 0
when not matched then
insert ( ParentId, ListId, NodeText ) values ( ParentId, #ListId, NodeText )
output T.NodeId, inserted.NodeId into #Fixups;
select * from #Nodes;
select * from #Fixups;
-- Apply the fixups to the new copy.
update Nodes
set ParentId = Fixups.NewNodeId
from #Nodes as Nodes inner join
-- Update only the copy and not the template. (Could also use IN or EXISTS.)
#Fixups as Copy on Copy.NewNodeId = Nodes.NodeId inner join
-- Map the old nodes to the new.
#Fixups as Fixups on Fixups.OldNodeId = Nodes.ParentId;
select * from #Nodes;
I have a database containing a hierarchy of categories stored using the adjacency list model.
The hierarchy is 3 levels deep (not including an imaginary root node) and contains approx 1700 nodes. Nodes in the 2nd and 3rd levels can have multiple parents. A additional table is used for the many-to-many relationship as below:
CREATE TABLE dbo.Category(
id int IDENTITY(1,1) NOT NULL,
name varchar(255) NOT NULL,
)
CREATE TABLE dbo.CategoryHierarchy(
relId int IDENTITY(1,1) NOT NULL,
catId int NOT NULL,
parentId int NOT NULL,
)
If I move to using the transitive closure table method (for the sake of data integrity etc) is there a relatively easy query I can execute that would generate the values for the closure table? (using SQL Server 2005)
I've look through articles and presentations such as Bill Karwin's Models for hierarchical data but that only has insertion queries for a single node and it would take forever for me to create my tree like that.
Thanks.
EDIT:
RelID in the CategoryHierarchy table is purely for the sake of a primary key, it has no bearing on the node ids of the Category table.
Also by closure table, I mean a table like this:
CREATE TABLE ClosureTable (
ancestor int NOT NULL,
descendant int NOT NULL,
[length] int NOT NULL,
)
Where the first two columns are a compound primary key, and are individually foreign keys to Category.id.
I was trying to figure out the same thing, but wanted it in a recursive CTE. This wouldn't have worked for you (SQL Server 2008+), but here's what I ended up with for anyone else looking.
The key is that the anchors aren't your root nodes (where parent_id IS NULL), but instead all your zero depth rows-to-be in the closure table.
Table
CREATE TABLE dbo.category (
id INT IDENTITY(1, 1) NOT NULL,
parent_id INT NULL
)
Data
INSERT INTO dbo.category (id, parent_id)
VALUES
(1, NULL),
(2, 1),
(3, 1),
(4, 2)
CTE
WITH category_cte AS
(
SELECT
id AS ancestor,
id AS descendant,
0 AS depth
FROM dbo.category
UNION ALL
SELECT
CTE.ancestor AS ancestor,
C.id AS descendant,
CTE.depth + 1 AS depth
FROM dbo.category AS C
JOIN category_cte AS CTE
ON C.parent_id = CTE.descendant
)
SELECT * FROM category_cte
Result
ancestor descendant depth
-------- ---------- -----
1 1 0 <- anchor query
2 2 0
3 3 0
4 4 0
2 4 1 <- first recursive query
1 2 1
1 3 1
1 4 2 <- second recursive query
I think I've been able to work out the solution myself.
If anyone has a better way of doing this, please comment.
IF OBJECT_ID('dbo.ClosureTable', 'U') IS NOT NULL
DROP TABLE dbo.ClosureTable
GO
CREATE TABLE dbo.ClosureTable (
ancestor int NOT NULL,
descendant int NOT NULL,
distance int NULL
)
GO
DECLARE #depth INT
SET #depth = 1
INSERT INTO dbo.ClosureTable (ancestor, descendant, distance)
SELECT catid, catid, 0 FROM dbo.Category -- insert all the self-referencing nodes
WHILE (#depth < 4) -- my tree is only 4 levels deep, i.e 0 - 3
BEGIN
INSERT INTO dbo.ClosureTable (ancestor, descendant, distance)
SELECT ct.ancestor, h.catid, #depth
FROM dbo.ClosureTable ct INNER JOIN dbo.CategoryHierarchy h ON ct.descendant = h.parentid
WHERE ct.distance = #depth - 1
SET #depth = #depth + 1
END
Cheers :)
WITH Children (id, name, iteration) AS (
SELECT id, name, 0
FROM Category
--WHERE id = #folderid -- if you want a startpoint
UNION ALL
SELECT b.id, b.name, a.iteration + 1
FROM Children AS a, Category AS b, CategoryHierarchy c
WHERE a.id = c.parentId
AND b.id = c.catId
)
SELECT id, name, iteration FROM Children
Not tested, but should work like this. At least a start how you do this fast without loops.
EDIT: I missred, not relId, it is parentId that has to link to Childrens table. But the result should be a table with all tables. So this is not what your originally wanted?
Hi I have a table which references itself and I need to be able to select the parent and all it's child records from a given parent Id.
My table is as follows:
ID | ParentID | Name
-----------------------
1 NULL A
2 1 B-1
3 1 B-2
4 2 C-1
5 2 C-2
So for the above example I'd like to be able to pass in a value of 1 and get all the records above.
So far, I've come up with the following recursive table-valued-function but it's not behaving as expected (only returning the first record).
CREATE FUNCTION [dbo].[SelectBranches]
(
#id INT
,#parentId INT
)
RETURNS #branchTable TABLE
(
ID INT
,ParentID INT
,Name INT
)
AS
BEGIN
IF #branchId IS NOT NULL BEGIN
INSERT INTO #branchTable
SELECT
ID
,ParentID
,Name
FROM
tblLinkAdvertiserCity
WHERE
ID = #id
END
INSERT INTO #branchTable
SELECT
br.ID
,br.ParentID
,br.Name
FROM
#branchTable b
CROSS APPLY
dbo.SelectBranches(NULL, b.ParentID) br
RETURN
END
GO
You can try this
DECLARE #Table TABLE(
ID INT,
ParentID INT,
NAME VARCHAR(20)
)
INSERT INTO #Table (ID,ParentID,[NAME]) SELECT 1, NULL, 'A'
INSERT INTO #Table (ID,ParentID,[NAME]) SELECT 2, 1, 'B-1'
INSERT INTO #Table (ID,ParentID,[NAME]) SELECT 3, 1, 'B-2'
INSERT INTO #Table (ID,ParentID,[NAME]) SELECT 4, 2, 'C-1'
INSERT INTO #Table (ID,ParentID,[NAME]) SELECT 5, 2, 'C-2'
DECLARE #ID INT
SELECT #ID = 2
;WITH ret AS(
SELECT *
FROM #Table
WHERE ID = #ID
UNION ALL
SELECT t.*
FROM #Table t INNER JOIN
ret r ON t.ParentID = r.ID
)
SELECT *
FROM ret
Recursion in CTE looks bit expensive, so I have wrote this function which make use of recursive function call but much faster that CTE recursion.
CREATE FUNCTION [dbo].[Fn_GetSubCategories]
(
#p_ParentCategoryId INT
) RETURNS #ResultTable TABLE
(
Id INT
)
AS
BEGIN
--Insert first level subcategories.
INSERT INTO #ResultTable
SELECT Id FROM Category WHERE ParentCategoryId = #p_ParentCategoryId OR Id = #p_ParentCategoryId
DECLARE #Id INT
DECLARE #ParentCategory TABLE(Id INT)
DECLARE cur_categories CURSOR
LOCAL STATIC READ_ONLY FORWARD_ONLY FOR
SELECT Id FROM Category WHERE ParentCategoryId = #p_ParentCategoryId and Id != #p_ParentCategoryId
OPEN cur_categories
IF ##CURSOR_ROWS > 0
BEGIN
FETCH NEXT FROM cur_categories INTO #Id
WHILE ##FETCH_STATUS = 0
BEGIN
--Insert remaining level sub categories.
IF EXISTS(SELECT 1 FROM Category WHERE ParentCategoryId = #Id AND Id != #Id)
BEGIN
INSERT INTO #ResultTable
SELECT DISTINCT C.Id from Fn_GetSubCategories(#Id) C INNER JOIN #ResultTable R ON C.Id != R.Id
END
FETCH NEXT FROM cur_categories INTO #Id
END
--Delete duplicate records
;WITH CTE AS
(SELECT *,ROW_NUMBER() OVER (PARTITION BY Id ORDER BY Id) AS RN FROM #ResultTable)
DELETE FROM CTE WHERE RN<>1
END
CLOSE cur_categories
DEALLOCATE cur_categories
RETURN
END
Unless you are using Oracle, your table structure is not suitable for the problem described. What you are attempting to do is grab a hierarchy (traversing a tree structure).
There is an article, More Trees & Hierarchies in SQL, that describes one method of solving the hierarchy problem. He basically adds a "lineage" column describing the hierarchy to every row.
Suppose I have a recursive table (e.g. employees with managers) and a list of size 0..n of ids. How can I find the lowest common parent for these ids?
For example, if my table looks like this:
Id | ParentId
---|---------
1 | NULL
2 | 1
3 | 1
4 | 2
5 | 2
6 | 3
7 | 3
8 | 7
Then the following sets of ids lead to the following results (the first one is a corner case):
[] => 1 (or NULL, doesn't really matter)
[1] => 1
[2] => 2
[1,8] => 1
[4,5] => 2
[4,6] => 1
[6,7,8] => 3
How to do this?
EDIT: Note that parent isn't the correct term in all cases. It's the lowest common node in all paths up the tree. The lowest common node can also be a node itself (for example in the case [1,8] => 1, node 1 is not a parent of node 1 but node 1 itself).
Kind regards,
Ronald
Here's one way of doing it; it uses a recursive CTE to find the ancestry of a node, and uses "CROSS APPLY" over the input values to get the common ancestry; you just change the values in #ids (table variable):
----------------------------------------- SETUP
CREATE TABLE MyData (
Id int NOT NULL,
ParentId int NULL)
INSERT MyData VALUES (1,NULL)
INSERT MyData VALUES (2,1)
INSERT MyData VALUES (3,1)
INSERT MyData VALUES (4,2)
INSERT MyData VALUES (5,2)
INSERT MyData VALUES (6,3)
INSERT MyData VALUES (7,3)
INSERT MyData VALUES (8,7)
GO
CREATE FUNCTION AncestorsUdf (#Id int)
RETURNS TABLE
AS
RETURN (
WITH Ancestors (Id, ParentId)
AS (
SELECT Id, ParentId
FROM MyData
WHERE Id = #Id
UNION ALL
SELECT md.Id, md.ParentId
FROM MyData md
INNER JOIN Ancestors a
ON md.Id = a.ParentId
)
SELECT Id FROM Ancestors
);
GO
----------------------------------------- ACTUAL QUERY
DECLARE #ids TABLE (Id int NOT NULL)
DECLARE #Count int
-- your data (perhaps via a "split" udf)
INSERT #ids VALUES (6)
INSERT #ids VALUES (7)
INSERT #ids VALUES (8)
SELECT #Count = COUNT(1) FROM #ids
;
SELECT TOP 1 a.Id
FROM #ids
CROSS APPLY AncestorsUdf(Id) AS a
GROUP BY a.Id
HAVING COUNT(1) = #Count
ORDER BY a.ID DESC
Update if the nodes aren't strictly ascending:
CREATE FUNCTION AncestorsUdf (#Id int)
RETURNS #result TABLE (Id int, [Level] int)
AS
BEGIN
WITH Ancestors (Id, ParentId, RelLevel)
AS (
SELECT Id, ParentId, 0
FROM MyData
WHERE Id = #Id
UNION ALL
SELECT md.Id, md.ParentId, a.RelLevel - 1
FROM MyData md
INNER JOIN Ancestors a
ON md.Id = a.ParentId
)
INSERT #result
SELECT Id, RelLevel FROM Ancestors
DECLARE #Min int
SELECT #Min = MIN([Level]) FROM #result
UPDATE #result SET [Level] = [Level] - #Min
RETURN
END
GO
and
SELECT TOP 1 a.Id
FROM #ids
CROSS APPLY AncestorsUdf(Id) AS a
GROUP BY a.Id, a.[Level]
HAVING COUNT(1) = #Count
ORDER BY a.[Level] DESC
After doing some thinking and some hints in the right direction from Marc's answer (thanks), I came up with another solution myself:
DECLARE #parentChild TABLE (Id INT NOT NULL, ParentId INT NULL);
INSERT INTO #parentChild VALUES (1, NULL);
INSERT INTO #parentChild VALUES (2, 1);
INSERT INTO #parentChild VALUES (3, 1);
INSERT INTO #parentChild VALUES (4, 2);
INSERT INTO #parentChild VALUES (5, 2);
INSERT INTO #parentChild VALUES (6, 3);
INSERT INTO #parentChild VALUES (7, 3);
INSERT INTO #parentChild VALUES (8, 7);
DECLARE #ids TABLE (Id INT NOT NULL);
INSERT INTO #ids VALUES (6);
INSERT INTO #ids VALUES (7);
INSERT INTO #ids VALUES (8);
DECLARE #count INT;
SELECT #count = COUNT(1) FROM #ids;
WITH Nodes(Id, ParentId, Depth) AS
(
-- Start from every node in the #ids collection.
SELECT pc.Id , pc.ParentId , 0 AS DEPTH
FROM #parentChild pc
JOIN #ids i ON pc.Id = i.Id
UNION ALL
-- Recursively find parent nodes for each starting node.
SELECT pc.Id , pc.ParentId , n.Depth - 1
FROM #parentChild pc
JOIN Nodes n ON pc.Id = n.ParentId
)
SELECT n.Id
FROM Nodes n
GROUP BY n.Id
HAVING COUNT(n.Id) = #count
ORDER BY MIN(n.Depth) DESC
It now returns the entire path from the lowest common parent to the root node but that is a matter of adding a TOP 1 to the select.