Sql HierarchyId How do I get the last descendants? - sql

Using t-sql hierarchy Id how do I get all of the rows that have no children (that is the last decendants)?
Say my table is structured like this:
Id,
Name,
HierarchyId
And has these rows:
1, Craig, /
2, Steve, /1/
3, John, /1/1/
4, Sam, /2/
5, Matt, /2/1/
6, Chris, /2/1/1/
What query would give me John and Chris?

Perhaps there are better ways but this seams to do the job.
declare #T table
(
ID int,
Name varchar(10),
HID HierarchyID
)
insert into #T values
(1, 'Craig', '/'),
(2, 'Steve', '/1/'),
(3, 'John', '/1/1/'),
(4, 'Sam', '/2/'),
(5, 'Matt', '/2/1/'),
(6, 'Chris', '/2/1/1/')
select *
from #T
where HID.GetDescendant(null, null) not in (select HID
from #T)
Result:
ID Name HID
----------- ---------- ---------------------
3 John 0x5AC0
6 Chris 0x6AD6
Update 2012-05-22
Query above will fail if node numbers is not in an unbroken sequence. Here is another version that should take care of that.
declare #T table
(
ID int,
Name varchar(10),
HID HierarchyID
)
insert into #T values
(1, 'Craig', '/'),
(2, 'Steve', '/1/'),
(3, 'John', '/1/1/'),
(4, 'Sam', '/2/'),
(5, 'Matt', '/2/1/'),
(6, 'Chris', '/2/1/2/') -- HID for this row is changed compared to above query
select *
from #T
where HID not in (select HID.GetAncestor(1)
from #T
where HID.GetAncestor(1) is not null)

Since you only need leafs and you don't need to get them from a specific ancestor, a simple non-recursive query like this should do the job:
SELECT * FROM YOUR_TABLE PARENT
WHERE
NOT EXISTS (
SELECT * FROM YOUR_TABLE CHILD
WHERE CHILD.HierarchyId = PARENT.Id
)
In plain English: select every row without a child row.
This assumes your HierarchyId is a FOREIGN KEY towards the Id, not the whole "path" as presented in your example. If it isn't, this is probably the first thing you should fix in your database model.
--- EDIT ---
OK, here is the MS SQL Server-specific query that actually works:
SELECT * FROM YOUR_TABLE PARENT
WHERE
NOT EXISTS (
SELECT * FROM YOUR_TABLE CHILD
WHERE
CHILD.Id <> PARENT.Id
AND CHILD.HierarchyId.IsDescendantOf(PARENT.HierarchyId) = 1
)
Note that the IsDescendantOf considers any row a descendant of itself, so we also need the CHILD.Id <> PARENT.Id in the condition.

Hi I use this one and works perfectly for me.
CREATE TABLE [dbo].[Test]([Id] [hierarchyid] NOT NULL, [Name] [nvarchar](50) NULL)
DECLARE #Parent AS HierarchyID = CAST('/2/1/' AS HierarchyID) -- Get Current Parent
DECLARE #Last AS HierarchyID
SELECT #Last = MAX(Id) FROM Test WHERE Id.GetAncestor(1) = #Parent -- Find Last Id for this Parent
INSERT INTO Test(Id,Name) VALUES(#Parent.GetDescendant(#Last, NULL),'Sydney') -- Insert after Last Id

Related

Lookup the MIN value in a SQL recursive query at the parent level?

I have a standard recursive lookup script that uses a CTE, a "union all", parent-child relationship. The problem I am trying to solve is that I would like to capture two attributes from the lowest level child record and place it on the highest level parent record.
The attributes are:
The maximum depth of the parent's child levels. In my data, not all parents have the same levels of depth in the child records. Some only span 1 level below the parent and some span as far as 10 levels below. On the output of my query, I would like to see the parent record contain the maximum number of levels of depth in their children.
All records in my data contain a field value, but this value is only important on the lowest level of the hierarchy. I would like the query to output the value of the lowest level child on the parent record.
Table:
childid, name, keyfield, parentid, level
11, Sally, xyz123, null, 1
1, Alex, xyz456, 11, 2
7, John, abc123, null, 1
3, Erin, abc456, 7, 2
4, Jen, abc789, 3, 3
Desired output:
childid, name, keyfield_max, keyfield_min
11, Sally, xyz123, xyz456
7, John, abc123, abc789
SQL Server Version:
DECLARE #data TABLE(childid int, [name] nvarchar(128), keyfield varchar(30), parentid int, [level] int);
INSERT #data VALUES(11, 'Sally', 'xyz123', null, 1), (1, 'Alex', 'xyz456', 11, 2), (7, 'John', 'abc123', null, 1), (3, 'Erin', 'abc456', 7, 2), (4, 'Jen', 'abc789', 3, 3);
WITH cte AS (
SELECT d.*, d.childid AS topid FROM #data d WHERE d.parentid IS NULL
UNION ALL
SELECT d.*, cte.topid FROM cte INNER JOIN #data d ON cte.childid = d.parentid
)
SELECT
d.childid, d.[name], t.keyfield_max, t.keyfield_min
FROM #data d
OUTER APPLY (SELECT MAX(cte.keyfield) AS keyfield_max, MIN(cte.keyfield) AS keyfield_min FROM cte WHERE cte.topid = d.childid) t
WHERE d.parentid IS NULL

filter data having more than 1 record

I have a table comprising departments;and the audit of persons added/removed from it.
deptid|personid|actionid|lastupdate
3|5678|i|....
3|5765|i|...
3|8796|i|...
3|5463|i|...
3|5678|r|.....
4|5678|i|....
In a particular department,I need to find out the audit for all those persons who have been actioned MORE THAN ONCE for a given department.
Note that a person can be allocated against multiple departments.
So in the above data,the result expected is:
3|5678|i|....
3|5678|r|.....
I tried the below - but do not know how to proceed to filter further
select personId,actionid,lastUpdate,RN=ROW_NUMBER()
OVER (PARTITION BY personId ORDER BY lastUpdate)
from DeptAudit where deptId=3
Probably this can help:
SELECT personId,actionid,lastUpdate
FROM DeptAudit
WHERE personid IN
(SELECT personId
FROM DeptAudit
GROUP BY personId
HAVING COUNT(*) > 1)
DECLARE #Widget TABLE
(ID INT,
Widget INT,
PART NVARCHAR(10));
INSERT INTO #Widget VALUES
(3, 56757, 'i'),
(3, 56755, 'i'),
(3, 56759, 'i'),
(3, 56753,'i'),
(3, 5678, 'r');
;WITH CTE AS (
select ID,Widget,PART,RN = ROW_NUMBER()OVER(PARTITION BY PART ORDER BY Widget desc) from #Widget
)select ID,Widget,PART from CTE WHERE RN = 1

Recursive select in SQL

I have an issue I just can't get my head around. I know what I want, just simply can't get it out on the screen.
What I have is a table looking like this:
Id, PK UniqueIdentifier, NotNull
Name, nvarchar(255), NotNull
ParentId, UniqueIdentifier, Null
ParentId have a FK to Id.
What I want to accomplish is to get a flat list of all the id's below the Id I pass in.
example:
1 TestName1 NULL
2 TestName2 1
3 TestName3 2
4 TestName4 NULL
5 TestName5 1
The tree would look like this:
-1
-> -2
-> -3
-> -5
-4
If I now ask for 4, I would only get 4 back, but if I ask for 1 I would get 1, 2, 3 and 5.
If I ask for 2, I would get 2 and 3 and so on.
Is there anyone who can point me in the right direction. My brain is fried so I appreciate all help I can get.
declare #T table(
Id int primary key,
Name nvarchar(255) not null,
ParentId int)
insert into #T values
(1, 'TestName1', NULL),
(2, 'TestName2', 1),
(3, 'TestName3', 2),
(4, 'TestName4', NULL),
(5, 'TestName5', 1)
declare #Id int = 1
;with cte as
(
select T.*
from #T as T
where T.Id = #Id
union all
select T.*
from #T as T
inner join cte as C
on T.ParentId = C.Id
)
select *
from cte
Result
Id Name ParentId
----------- -------------------- -----------
1 TestName1 NULL
2 TestName2 1
5 TestName5 1
3 TestName3 2
Here's a working example:
declare #t table (id int, name nvarchar(255), ParentID int)
insert #t values
(1, 'TestName1', NULL),
(2, 'TestName2', 1 ),
(3, 'TestName3', 2 ),
(4, 'TestName4', NULL),
(5, 'TestName5', 1 );
; with rec as
(
select t.name
, t.id as baseid
, t.id
, t.parentid
from #t t
union all
select t.name
, r.baseid
, t.id
, t.parentid
from rec r
join #t t
on t.ParentID = r.id
)
select *
from rec
where baseid = 1
You can filter on baseid, which contains the start of the tree you're querying for.
Try this:
WITH RecQry AS
(
SELECT *
FROM MyTable
UNION ALL
SELECT a.*
FROM MyTable a INNER JOIN RecQry b
ON a.ParentID = b.Id
)
SELECT *
FROM RecQry
Here is a good article about Hierarchy ID models. It goes right from the start of the data right through to the query designs.
Also, you could use a Recursive Query using a Common Table Expression.
I'm guessing that the easiest way to accomplish what you're looking for would be to write a recursive query using a Common Table Expression:
MSDN - Recursive Queries Using Common Table Expressions

Sorting tree with other column in SQL Server 2008

I have a table which implements a tree using hierarchyid column
Sample data:
People \
Girls \1\
Zoey \1\1\
Kate \1\2\
Monica \1\3\
Boys \2\
Mark \2\1\
David \2\2\
This is the order using hierarchyid column as sort column
I would like to sort data using hierarchyid but also using name so it would look like this:
People \
Boys \2\
David \2\2\
Mark \2\1\
Girls \1\
Kate \1\2\
Monica \1\3\
Zoey \1\1\
Is there a simple solution to do this?
Can it be done with a single SQL query?
Rewrite your query as a recursive CTE:
DECLARE #table TABLE (id INT NOT NULL PRIMARY KEY, name NVARCHAR(4000) NOT NULL, path HIERARCHYID)
INSERT
INTO #table
VALUES
(1, 'People', '/'),
(2, 'Girls', '/1/'),
(3, 'Boys', '/2/'),
(4, 'Zoey', '/1/1/'),
(5, 'Kate', '/1/2/'),
(6, 'Monica', '/1/3/'),
(7, 'Mark', '/2/1/'),
(8, 'David', '/2/2/')
;WITH q AS
(
SELECT *, HIERARCHYID::Parse('/') AS newpath
FROM #table
WHERE path = HIERARCHYID::GetRoot()
UNION ALL
SELECT t.*, HIERARCHYID::Parse(q.newpath.ToString() + CAST(ROW_NUMBER() OVER (ORDER BY t.name) AS NVARCHAR(MAX)) + '/')
FROM q
JOIN #table t
ON t.path.IsDescendantOf(q.path) = 1
AND t.path.GetLevel() = q.path.GetLevel() + 1
)
SELECT *
FROM q
ORDER BY
newpath

SQL - Ordering by multiple criteria

I have a table of categories. Each category can either be a root level category (parent is NULL), or have a parent which is a root level category. There can't be more than one level of nesting.
I have the following table structure:
Categories Table Structure http://img16.imageshack.us/img16/8569/categoriesi.png
Is there any way I could use a query which produced the following output:
Free Stuff
Hardware
Movies
CatA
CatB
CatC
Software
Apples
CatD
CatE
So the results are ordered by top level category, then after each top level category, subcategories of that category are listed?
It's not really ordering by Parent or Name, but a combo of the two. I'm using SQL Server.
It seems to me like you are looking to flatten and order your hierarchy, the cheapest way to get this ordering would be to store an additional column in the table that has the full path.
So for example:
Name | Full Path
Free Stuff | Free Stuff
aa2 | Free Stuff - aa2
Once you store the full path, you can order on it.
If you only have a depth of one you can auto generate a string to this effect with a single subquery (and order on it), but this solution does not work that easily when it gets deep.
Another option, is to move this all over to a temp table and calculate the full path there, on demand. But it is fairly expensive.
You could make the table look at itself, ordering by the parent Name then the child Name.
select categories.Name AS DisplayName
from categories LEFT OUTER JOIN
categories AS parentTable ON categories.Parent = parentTable.ID
order by parentTable.Name, DisplayName
Ok, here we go :
with foo as
(
select 1 as id, null as parent, 'CatA' as cat from dual
union select 2, null, 'CatB' from dual
union select 3, null, 'CatC' from dual
union select 4, 1, 'SubCatA_1' from dual
union select 5, 1, 'SubCatA_2' from dual
union select 6, 2, 'SubCatB_1' from dual
union select 7, 2, 'SubCatB_2' from dual
)
select child.cat
from foo parent right outer join foo child on parent.id = child.parent
order by case when parent.id is not null then parent.cat else child.cat end,
case when parent.id is not null then 1 else 0 end
Result :
CatA
SubCatA_1
SubCatA_2
CatB
SubCatB_1
SubCatB_2
CatC
Edit - Solution change inspire from van's order by ! Much simpler that way.
Not entirely sure of your questions but it sounds like PARTITION BY might be useful for you. There's a good introductory post on PARTITION BY here.
Here you have a complete working example using a resursive common table expression.
DECLARE #categories TABLE
(
ID INT NOT NULL,
[Name] VARCHAR(50),
Parent INT NULL
);
INSERT INTO #categories VALUES (4, 'Free Stuff', NULL);
INSERT INTO #categories VALUES (1, 'Hardware', NULL);
INSERT INTO #categories VALUES (3, 'Movies', NULL);
INSERT INTO #categories VALUES (2, 'Software', NULL);
INSERT INTO #categories VALUES (10, 'a', 0);
INSERT INTO #categories VALUES (12, 'apples', 2);
INSERT INTO #categories VALUES (8, 'catD', 2);
INSERT INTO #categories VALUES (9, 'catE', 2);
INSERT INTO #categories VALUES (5, 'catA', 3);
INSERT INTO #categories VALUES (6, 'catB', 3);
INSERT INTO #categories VALUES (7, 'catC', 3);
INSERT INTO #categories VALUES (11, 'aa2', 4);
WITH categories(ID, Name, Parent, HierarchicalName)
AS
(
SELECT
c.ID
, c.[Name]
, c.Parent
, CAST(c.[Name] AS VARCHAR(200)) AS HierarchicalName
FROM #categories c
WHERE c.Parent IS NULL
UNION ALL
SELECT
c.ID
, c.[Name]
, c.Parent
, CAST(pc.HierarchicalName + c.[Name] AS VARCHAR(200))
FROM #categories c
JOIN categories pc ON c.Parent = pc.ID
)
SELECT c.*
FROM categories c
ORDER BY c.HierarchicalName
SELECT
ID,
Name,
Parent,
RIGHT(
'000000000000000' +
CASE WHEN Parent IS NULL
THEN CONVERT(VARCHAR, Id)
ELSE CONVERT(VARCHAR, Parent)
END, 15
)
+ '_' + CASE WHEN Parent IS NULL THEN '0' ELSE '1' END
+ '_' + Name
FROM
categories
ORDER BY
4
The long padding is to account for the fact that SQL Server's INT data type goes from 2,147,483,648 through 2,147,483,647.
You can ORDER BY the expression directly, no need to use ORDER BY 4. It was just to show what it is sorting on.
It is worth noting that this expression cannot use any index. This means sorting a large table will be slow.