T-SQL hierarchy query - sql-server

I have a table with hierarchical data:
This is a sample of data with id, parent id, name, code (which is sometimes not filled), level and isroot column. In real scenario there will be more levels than just 2 but now let's look at the simplified example.
What I need to do is to loop over all records and find rows where id is not filled at any level of the hierarchy:
Rows with id ranging should be returned from 6 till 10 because they do not have code filled in at any point of the hierarchy
Rows from 1 till 5 should not be returned because code was mentioned somewhere in the hierarchy.
How should one solve this problem with T-SQL?
The only solution that came to my mind was recursivity (cte or WHILE) but I what I was trying to implement was too complex and did not solve the problem.

Slightly different than #DhruvJoshi's answer, since it may be useful:
WITH recCTE AS
(
SELECT
id,
parent_id,
CASE WHEN CODE IS NOT NULL THEN 1 ELSE 0 END as code_check,
1 as depth,
CAST(id as VARCHAR(50)) as path
FROM table
WHERE isRootLevel = 1
UNION ALL
SELECT
table.id,
table.parent_id,
CASE WHEN CODE IS NOT NULL OR reccte.code_check = 1 THEN 1 ELSE 0 END,
depth + 1 as depth,
reccte.path + CAST(table.id AS varchar(10)) as path
FROM
recCTE
INNER JOIN table ON
recCTE.ID = table.parent_id
WHERE depth < 20 /*just in case you start cycling/endless looping*/
)
SELECT * FROM recCTE where code_check = 0 ORDER BY path, depth;

Here is another example, for anyone who may still struggling with hierarchical data (like me).
Say we got a following hierarchical structure:
CEO
|-- Sales Director
│ |-- Sales Manager 1
│ `-- Sales Manager 2
`-- Technical Director
|-- Product Manager
|-- R&D Team Lead
`-- QA Team Lead
Using recursive cte to get the level for each node:
with cte as (
select id, parentId, roleName, 1 as lvl from roles where id = 1 -- root node
union all
select r.id, r.parentId, r.roleName, cte.lvl+1 as lvl from roles r -- child nodes
inner join cte on cte.id = r.parentid
)
select * from cte;
To get the path for each node:
with cte as (
select id, roleName, cast(roleName as varchar(200)) as hierPath
from roles where id = 1
union all
select r.id, r.rolename, cast(cte.hierPath + ' / ' + r.rolename as varchar(200)) as hierPath
from roles r
inner join cte on cte.id = r.parentid
)
select * from cte;
Using row_number() and power() to get the sorted hierarchy tree result (parent followed by all of its children, then all the children of each one of those and so on):
with cte as (
select id, roleName, cast(roleName as varchar(200)) as hierPath, 1 as lvl,
row_number()over(partition by parentid order by roleName) / power(10.0,1) as sortNo
from roles where id = 1
union all
select r.id, r.rolename, cast(cte.hierPath + ' / ' + r.rolename as varchar(200)) as hierPath, cte.lvl+1 as lvl,
cte.sortNo + row_number()over(partition by r.parentid order by r.roleName) / power(10.0,cte.lvl+1) as sortNo
from roles r
inner join cte on cte.id = r.parentid
)
select * from cte
order by sortNo;
Setup test data:
create table roles (
id int not null,
parentId int,
roleName varchar(50) not null
);
insert into roles
(id, parentId, roleName)
values
(1, null, 'CEO'),
(2, 1, 'Sales Director'),
(3, 1, 'Technical Director'),
(4, 2, 'Sales Manager 1'),
(5, 2, 'Sales Manager 2'),
(6, 3, 'Product Manager'),
(7, 3, 'R&D Team Lead'),
(8, 3, 'QA Team Lead');

Query like this should work:
; with cte as
(
select id, parent_id,code,parent_id as RootId from tableT where IsRootLevel=1
UNION ALL
select T2.id,T2.parent_id,T2.code,T1.RootId as RootId from tableT T2 join
cte T1 on T1.id=T2.parent_id and IsRootLevel=0
)
,
cte2 as
(select id,MAX(case when code ='' then NULL else code end) over( partition by RootId) as code from cte)
select T1.* from tableT T1 left join cte2 T2
on T1.id=T2.id
where T2.code is NULL
See working demo

Related

T-SQL Left join twice

Query below works as planned, it shows exactly the way i joined it, and that is fine, but problem with it, is that if you have more "specialization" tables for users, something like "Mail type" or anything that user can have more then one data ... you would have to go two left joins for each and "give priority" via ISNULL (in this case)
I am wondering, how could I avoid using two joins and "give" priority to TypeId 2 over TypeId 1 in a single join, is that even possible?
if object_id('tempdb..#Tab1') is not null drop table #Tab1
create table #Tab1 (UserId int, TypeId int)
if object_id('tempdb..#Tab2') is not null drop table #Tab2
create table #Tab2 (TypeId int, TypeDescription nvarchar(50))
insert into #Tab1 (UserId, TypeId)
values
(1, 1),
(1, 2)
insert into #Tab2 (TypeId, TypeDescription)
values
(1, 'User'),
(2, 'Admin')
select *, ISNULL(t2.TypeDescription, t3.TypeDescription) [Role]
from #Tab1 t1
LEFT JOIN #Tab2 t2 on t1.TypeId = t2.TypeId and
t2.TypeId = 2
LEFT JOIN #Tab2 t3 on t1.TypeId = t3.TypeId and
t3.TypeId = 1
The first problem is determining priority. In this case, you could use the largest TypeId, but that does not seem like a great idea. You could add another column to serve as a priority ordinal instead.
From there, it is a top 1 per group query:
using top with ties and row_number():
select top 1 with ties
t1.UserId, t1.TypeId, t2.TypeDescription
from #Tab1 t1
left join #Tab2 t2
on t1.TypeId = t2.TypeId
order by row_number() over (
partition by t1.UserId
order by t2.Ordinal
--order by t1.TypeId desc
)
using common table expression and row_number():
;with cte as (
select t1.UserId, t1.TypeId, t2.TypeDescription
, rn = row_number() over (
partition by t1.UserId
order by t2.Ordinal
--order by t1.TypeId desc
)
from #Tab1 t1
left join #Tab2 t2
on t1.TypeId = t2.TypeId
)
select UserId, TypeId, TypeDescription
from cte
where rn = 1
rextester demo for both: http://rextester.com/KQAV36173
both return:
+--------+--------+-----------------+
| UserId | TypeId | TypeDescription |
+--------+--------+-----------------+
| 1 | 2 | Admin |
+--------+--------+-----------------+
Actually I don't think you don't need a join at all. But you have to take the max TypeID without respect to the TypeDescription, since these differences can defeat a Group By. So a workaround is to take the Max without TypeDescription initially, then subquery the result to get the TypeDescription.
SELECT dT.*
,(SELECT TypeDescription FROM #Tab2 T2 WHERE T2.TypeId = dT.TypeId) [Role] --2. Subqueries TypeDescription using the Max TypeID
FROM (
select t1.UserId
,MAX(T1.TypeId) [TypeId]
--, T1.TypeDescription AS [Role] --1. differences will defeat group by. Subquery for value later in receiving query.
from #Tab1 t1
GROUP BY t1.UserId
) AS dT
Produces Output:
UserId TypeId Role
1 2 Admin

How to change duplicate values to unique values in a column using by stored procedure in SQL Server

I have to change RowSpan value if OccupationalInjuryId's duplicate value exists. Here is the screenshot:
Should I use T-SQL or #temporary table structure. But I couldn't figure out how to fix. For example there are two values with OccupationalId = 1100 and both rowspan's are 2. It should be 1 for second value's RowSpan no.
My SQL query:
select *
from
(select
Row_Number() over (order by oi.Id) as RowNo,
oid.OccupationalInjuryId
from
OTH_OccupationalInjury oi
left join
OTH_OccupationalInjuryDetail oid on oi.Id = oid.OccupationalInjuryId
where
1 = 1) t1
left join
(select
count(OccupationalInjuryId) end as RowSpan,
oid.OccupationalInjuryId
from
OTH_OccupationalInjury oi
left join
OTH_OccupationalInjuryDetail oid on oi.Id = oid.OccupationalInjuryId
where
1=1
group by
OccupationalInjuryId
having
(Count(OccupationalInjuryId) > 1) ) t2 on t1.OccupationalInjuryId = t2.OccupationalInjuryId
END
If I got you correctly, you only care about first RowSpan value for each OccupationalInjuryId and for others you want 1. If I am wrong, correct me.
So, Can you try something like :
DECLARE #BaseTable TABLE(ID INT, OccupationalInjuryId INT, RowSpan INT)
INSERT INTO #BaseTable (ID, OccupationalInjuryId, RowSpan)
SELECT 1, 1100, 2 UNION ALL
SELECT 2, 1100, 2 UNION ALL
SELECT 3, 1099, 2 UNION ALL
SELECT 4, 1099, 3 UNION ALL
SELECT 5, 1106, NULL
SELECT
ID,
OccupationalInjuryId,
CASE WHEN rn = 1 THEN RowSpan ELSE 1 END AS RowSpan
FROM
(
SELECT ID, OccupationalInjuryId, RowSpan, ROW_NUMBER() OVER (PARTITION BY OccupationalInjuryId, RowSpan ORDER BY ID) AS rn FROM #BaseTable
) tmp

Show a unary relationship hierarchy in one field in MS SQL

I have a table called "Services" which contains a ServiceID, a ParentID and a Description, where the ParentID is the ServiceID of another record. The data is setup in such a way that it forms a multiple level hierarchy of items and the "Root" items have the ParentID set to zero. How can I have a query with a new field that shows a thread of all the parents up to the root parent for each record. Of course, root items will have this field as blank. Using cars as an example, I would like to have such text inside this field for the entry 'X3' and 'Punto' respectively:
Automobiles > Germany > BMW > 4 Wheel Drive > X3
Automobiles > Italy > FIAT > Front Wheel Drive > Punto
I suspect I should have a function to which I feed the ServiceID and which does the necessary recursion to get me the string value containing the threaded descriptions. Tried Googling unary relations but could not find an example with the code for the function I need.
Thanks in advance!
UPDATE:
Here is what my table looks like:
CREATE TABLE [dbo].[Services](
[ServiceID] [int] IDENTITY(1,1) NOT NULL,
[ParentID] [int] NULL,
[ServiceDescription] [nvarchar](250) NULL)
Here is a generic example. You can copy, paste and run it and it will show you the output. If this works you should be able to modify the table and column values to get it to work for your situation.
If Object_ID('dbo.Services') Is Not Null Drop Table [dbo].[Services];
Create Table [dbo].[Services] (ServiceID Int Identity(1,1) NOT NULL, ParenServiceID Int NULL, ServiceDescription Nvarchar(250) NULL);
Insert [dbo].[Services]
Select null, 'Automobiles'
Union All
Select 1, 'Germany'
Union All
Select 2, 'BMW'
Union All
Select 3, '4 Wheel Drive'
Union All
Select 1, 'Italy'
Union All
Select 5, 'FIAT'
Union All
Select 4, 'X3'
Union All
Select 6, 'Front Wheel Drive'
Union All
Select 8, 'Punto';
With recurCTE As
(
Select h.ServiceID, h2.ParenServiceID As nextParent, Convert(Varchar(max),Isnull(h2.ServiceDescription + ' > ','') + h.ServiceDescription) As Hierarchy
From [dbo].[Services] h
Left Join [dbo].[Services] h2
On h.ParenServiceID = h2.ServiceID
Union All
Select rc.ServiceID, h.ParenServiceID As nextParent, Convert(Varchar(max),Isnull(h.ServiceDescription + ' > ','') + rc.Hierarchy) As Hierarchy
From recurCTE rc
Join [dbo].[Services] h
On rc.nextParent = h.ServiceID
), orderedResults As
(
Select ServiceID, Hierarchy
From (Select Row_Number() Over (Partition By ServiceID Order By Len(Hierarchy) Desc) As lenPriID,
ServiceID,
Hierarchy
From recurCTE) As n
Where lenPriID = 1
)
Select h.*, o.Hierarchy
From orderedResults o
Join [dbo].[Services] h
On o.ServiceID = h.ServiceID
Where ServiceDescription In ('X3','Punto');
I would use left joins to some reasonable limit (like 20) and then concatatinate the strings:
declare #services table (ServiceID int , ParentID int,[Description] varchar(20))
insert #services
select 1,null,'automobiles' union
select 2,null,'hotels' union
select 3,1,'Germany' union
select 4,3,'BMW' union
select 5,3,'Audi' union
select 6,2,'Hawaii' union
select 7,2,'Australia'
select
s.Description+'>'+s2.Description+'>'+isnull(s3.Description,'')+'>'+isnull(s4.Description,'')
from #services s
left join #services s2 on (s2.ParentID=s.ServiceID)
left join #services s3 on (s3.ParentID=s2.ServiceID)
left join #services s4 on (s4.ParentID=s3.ServiceID)
left join #services s5 on (s5.ParentID=s4.ServiceID)

CTE to return all items in hierarchy

I have a table with a recursive hierarchy (i.e. ID, ParentID). For any item in this hierachy, I want to be able to bring back a list of everything UP AND DOWN the hierarchy along with the level for each row. Assume that a parent can only ever have a single child.
Take for example the following:
ID ParentID
--------------
1 NULL
2 1
3 2
4 NULL
5 4
6 5
Given ID 1, 2, or 3, I want to return:
ID ParentID Level
-----------------------
1 NULL 1
2 1 2
3 2 3
I've done this before, but I can't remember how. I know the solution involves a CTE, I just can't get it right! Any help is appreciated.
;with cte as
(
select *, 1 as level from #t where id = #yourid
union all
select t.*, level - 1
from cte
inner join #t t on cte.parent = t.id
),
cte2 as
(
select * from cte
union all
select t.*, level+1
from cte2
inner join #t t on cte2.id = t.parent
)
select id,parent, ROW_NUMBER() over (order by level) level
from ( select distinct id, parent, level from cte2) v
The most barebones version of the CTE query I could come up with is:
WITH Ancestry (AncestorID, DescendantID)
AS
(
SELECT
ParentID, ID
FROM
dbo.Location
WHERE
ParentID IS NOT NULL
UNION ALL
SELECT
P.AncestorID, C.ID
FROM
dbo.Location C
JOIN
Ancestry P on C.ParentID = P.DescendantID
)
SELECT * FROM Ancestry
The result is a list of all Ancestor/Descendant relationships that exist in the table.
The final "SELECT * FROM Ancestry" can be replaced with something more complex to filter, order, etc.
To include reflexive relationships, the query can be modified by adding two lines to the final SELECT statement:
SELECT * FROM Ancestry
UNION
SELECT ID, ID FROM dbo.Location
;WITH Recursive_CTE AS (
SELECT
child.ExecutiveId,
CAST(child.ExecutiveName as varchar(100)) BusinessUnit,
CAST(NULL as bigint) ParentUnitID,
CAST(NULL as varchar(100)) ParentUnit,
CAST('' as varchar(100)) LVL,
CAST(child.ExecutiveId as varchar(100)) Hierarchy,
1 AS RecursionLevel
FROM Sales_Executive_level child
WHERE ExecutiveId = 4000 --your Id which you want to get all parent node
UNION ALL
SELECT
child.ExecutiveId,
CAST(LVL + child.ExecutiveName as varchar(100)) AS BusinessUnit,
child.ParentExecutiveID,
parent.BusinessUnit ParentUnit,
CAST('' + LVL as varchar(100)) AS LVL,
CAST(Hierarchy + ':' + CAST(child.ExecutiveId as varchar(100)) as varchar(100)) Hierarchy,
RecursionLevel + 1 AS RecursionLevel
FROM Recursive_CTE parent
INNER JOIN Sales_Executive_level child ON child.ParentExecutiveID = parent.ExecutiveId
)
SELECT * FROM Recursive_CTE ORDER BY Hierarchy
OPTION (MAXRECURSION 300);

How to show recursive parentID in a single column in SQL

Here is the example structure of the table:
ID Name ParentID
-----------------------
1 Ancestor NULL
2 GrandFather 1
3 GrandMother 1
4 Child 3
I'm trying to write a query that would return
ID Name Family
----------------------------
1 Ancestor
2 GrandFather Ancestor
3 GrandMother Ancestor
4 Child Ancestor^GrandMother
The tricky part is that I want to show the family of all rows and in a top-down order.
If anyone can point me in the right direction, it would be appreciated :)
EDIT :: This is the real query, but it follows the same idea. it returns an error on line : marketparent.family + '^'+ t2.marketGroupName because it cant find marketparent
WITH marketparent ( marketGroupID,parentGroupID, marketGroupName,family)
AS
(
SELECT marketGroupID,
parentGroupID,
marketGroupName,
'' as family
FROM EVE.dbo.invMarketGroups
WHERE parentGroupID IS NULL
UNION ALL
SELECT t2.parentGroupID,
t2.marketGroupID,
t2.marketGroupName,
marketparent.family + '^'+ t2.marketGroupName
FROM EVE.dbo.invMarketGroups as t2
INNER JOIN marketparent as mp
ON mp.marketGroupID = t2.parentGroupID
)
-- Statement using the CTE
SELECT TOP 10 *
FROM marketparent;
You did not specify your DBMS, so I'm assuming PostgreSQL
WITH RECURSIVE fam_tree (id, name, parent, family) as
(
SELECT id,
name,
parentid,
''::text as family
FROM the_unknown_table
WHERE parent IS NULL
UNION ALL
SELECT t2.id,
t2.name,
t2.parentid,
fam_tree.family || '^' || t2.name
FROM the_unknown_table t2
INNER JOIN fam_tree ON fam_tree.id = t2.parentid
)
SELECT *
FROM fam_tree;
This is standard SQL (except for the ::text typecast) that should work with very few changes on most modern DBMS.
Edit:
For SQL Server you would need to replace the standard concatention character with Microsoft's non-standar + (and you need to remove the recursive keyword which is required by the standard but for some strange reason rejected by SQL Server)
WITH fam_tree (id, name, parent, family) as
(
SELECT id,
name,
parentid,
'' as family
FROM the_unknown_table
WHERE parent IS NULL
UNION ALL
SELECT t2.id,
t2.name,
t2.parentid,
fam_tree.family + '^' + t2.name
FROM the_unknown_table t2
INNER JOIN fam_tree ON fam_tree.id = t2.parentid
)
SELECT *
FROM fam_tree;
You can use a recursive Common Table Expression.
declare #T table
(
ID int,
Name nvarchar(15),
ParentID int
);
insert into #T values
(1, N'Ancestor', NULL),
(2, N'GrandFather', 1),
(3, N'GrandMother', 1),
(4, N'Child', 3);
with C as
(
select T.ID,
T.Name,
T.ParentID,
cast(N' ' as nvarchar(max)) as Family
from #T as T
where T.ParentID is null
union all
select T.ID,
T.Name,
T.ParentID,
C.Family+'^'+C.Name
from #T as T
inner join C
on T.ParentID = C.ID
)
select C.ID,
C.Name,
stuff(C.Family, 1, 2, '') as Family
from C;
select T.ID, T.Name, (select name from table where ID=T.ParentID)as Family
from table T

Resources