I have an employee table.
Employee
Supervisor
YearMonth
a
b
202201
b
c
202201
c
null
202201
e
b
202202
b
d
202202
And I want to create an org hierarchy that is at a column level in SQL expected output =
Employee
Supervisor
YearMonth
Org1
Org2
a
b
202201
b
c
b
c
202201
c
null
c
null
202201
null
null
e
b
202202
b
d
b
d
202202
d
null
I have been able to achieve this in python, but want the same output in SQL.
I've tried to get what you want but maybe you have provided dud data!?
as Employee b is listed twice so he has supervisor c and d, hence why the example below differ from what you want.
I've provided 3 types or approaches, run them all and see what you think, I think the third one is closest to what you want
IF EXISTS (SELECT * FROM tempdb.dbo.sysobjects O WHERE O.xtype in ('U') AND O.id = object_id(N'tempdb..#Employee'))
BEGIN
PRINT 'Removing temp table #Employee'
DROP TABLE #Employee;
END
CREATE TABLE #Employee (
EmployeeId VARCHAR(1),
SupervisorId VARCHAR(1),
YearMonth VARCHAR(6)
)
INSERT INTO #Employee (EmployeeId, SupervisorId, YearMonth)
VALUES('a', 'b', '202201'),
('b', 'c', '202201'),
('c', NULL, '202201'),
('e', 'b', '202202'),
('b', 'd', '202202')
-- SELECT * FROM #Employee
/*
Approach 1: Use a CTE to recursively get the hierarchy
*/
;WITH MyTable AS
(
SELECT EmployeeId, SupervisorId, 1 AS [Level]
FROM #Employee
WHERE EmployeeId = 'a' -- Remove this to get everyone and the levels of hierarchy
UNION ALL
SELECT e.EmployeeId, e.SupervisorId, T.[Level] + 1
FROM #Employee AS E
INNER JOIN MyTable AS T ON T.SupervisorId = E.EmployeeId
)
SELECT * FROM MyTable
ORDER BY [Level] ASC
/*
Approach 2:
What you are asking is just display the manager for employee and supervisor
But this has more data as supervisor b has more than one supervisor
*/
SELECT
E.EmployeeId AS Employee,
E.SupervisorId AS Supervisor,
E.YearMonth,
E.SupervisorId AS Org1,
E2.SupervisorId AS Org2
FROM #Employee AS E
LEFT JOIN #Employee AS E2 ON E2.EmployeeId = E.SupervisorId
GO
/*
Approach 3:
Using a outer apply will just get the data for each row
*/
SELECT
E.EmployeeId AS Employee,
E.SupervisorId AS Supervisor,
E.YearMonth,
E.SupervisorId AS Org1,
T.SupervisorId AS Org2
FROM #Employee AS E
OUTER APPLY (
SELECT TOP 1 *
FROM #Employee AS E2
WHERE E2.EmployeeId = E.SupervisorId
) AS T
Output to show, note the first one I just focus on Employee a but you can remove that to get everyone's hierarchy and display the level they are in.
Related
This is the Scenario : I have a duplicate rows in my table with the same Id , Name and so on .
1) I have to find the duplicate row matching all the criteria ( this is done)
2) Delete them only if the criteria match
3) Use the id of the deleted record and update the existing row in the table
For this i have created a 2 temp table. Temp1 is the table with all the record. Temp2 consist of duplicated row.
IF OBJECT_ID('tempdb..#Temp1') IS NOT NULL
DROP TABLE #Temp1
IF OBJECT_ID('tempdb..#Temp2') IS NOT NULL
DROP TABLE #Temp2
IF OBJECT_ID('tempdb..#Temp3') IS NOT NULL
DROP TABLE #Temp3
CREATE Table #Temp1 (
Id int,
Name NVARCHAR(64),
StudentNo INT NULL,
ClassCode NVARCHAR(8) NULL,
Section NVARCHAR(8) NULL,
)
INSERT INTO #Temp1 (Id, Name,StudentNo,ClassCode,Section) Values(1,'Joe',123,'A1', 'I')
INSERT INTO #Temp1 (Id, Name,StudentNo,ClassCode,Section) Values(1,'Joe',123,'A1', 'I')
INSERT INTO #Temp1 (Id, Name,StudentNo,ClassCode,Section) Values(2,'Harry',113,'X2', 'H')
INSERT INTO #Temp1 (Id, Name,StudentNo,ClassCode,Section) Values(2,'Harry',113,'X2', 'H')
INSERT INTO #Temp1 (Id, Name,StudentNo,ClassCode,Section) Values(3,'Elle',121,'J1', 'E1')
INSERT INTO #Temp1 (Id, Name,StudentNo,ClassCode,Section) Values(3,'Elle',121,'J1', 'E')
INSERT INTO #Temp1 (Id, Name,StudentNo,ClassCode,Section) Values(8,'Jane',191,'A1', 'E')
INSERT INTO #Temp1 (Id, Name,StudentNo,ClassCode,Section) Values(5,'Silva',811,'S1', 'SE')
INSERT INTO #Temp1 (Id, Name,StudentNo,ClassCode,Section) Values(6,'Juan',411,'S2', 'SE')
INSERT INTO #Temp1 (Id, Name,StudentNo,ClassCode,Section) Values(7,'Carla',431,'S2', 'SE')
;WITH CTE AS (
select
ROW_NUMBER() over (partition by Id
, StudentNo
order by Id, StudentNo)as Duplicate_RowNumber
, * from #Temp1 )
select t1.Id,t1.Name,t1.StudentNo,t1.Section,t1.ClassCode
INTO #Temp2
from CTE as c INNER JOIN #Temp1 as t1 ON t1.Id = c.Id
and t1.StudentNo = t1.StudentNo
and c.Duplicate_RowNumber >1
-- this will have 6 rows all the duplicates are included
--select * from #Temp2
-- this is for output clause
DECLARE #inserted Table (Id int,
Name NVARCHAR(64),
StudentNo INT NULL,
ClassCode NVARCHAR(8) NULL,
Section NVARCHAR(8) NULL)
DELETE FROM #temp1
OUTPUT deleted.Id , deleted.Name ,deleted.StudentNo ,deleted.ClassCode ,deleted.Section into #inserted
WHERE EXISTS ( SELECT * FROM #Temp2 as t2
where #temp1.Id = t2.Id
and #temp1.Name = t2.Name
and #temp1.StudentNo = t2.StudentNo
and #temp1.ClassCode = t2.ClassCode
and #temp1.Section = t2.Section)
-- this is to check what is delete so that i can join it and update the table temp1
select * from #inserted
You can see below the query should not delete the last two highlighted column because the Section does not match. It should only delete matching criteria from Temp1 and Temp2.
Scenario 2 : Delete the duplicate record in Temp1 and use the key in order to update the data to NULL for Section and Classcode . This is what i expect with the highlighted to be NULLs .
You can run this query yourself - just copy and paste.
Yes, for scenario #1 it is going to delete the rows because the problem is in this section.
I added this table for references.
Added this #temp2 table to clarify for later use.
CREATE Table #Temp2 (
Id int,
Name Varchar(64),
StudentNo INT NULL,
ClassCode Varchar(8) NULL,
Section Varchar(8) NULL,
)
IF OBJECT_ID('tempdb..#tmp4') IS NOT NULL
DROP TABLE #tmp4
select t1.Id,t1.Name,t1.StudentNo,t1.Section,t1.ClassCode,
Duplicate_RowNumber
INTO #Duplicatedata
from CTE as c INNER JOIN #Temp1 as t1 ON t1.Id = c.Id
and t1.StudentNo = t1.StudentNo
and c.Duplicate_RowNumber >1
select * from #Duplicatedata
This is going to satisfy both condition as #temp 1 will have both rows for Elle as your join condition is only on ID and Student No.
I added row number column for clarity.
Id Name StudentNo Section ClassCode Duplicate_RowNumber
1 Joe 123 I A1 2
1 Joe 123 I A1 2
2 Harry 113 H X2 2
2 Harry 113 H X2 2
3 Elle 121 E1 J1 2
3 Elle 121 E J1 2
As your Partition is based by Student No and ID, every duplicate row will have 2 or more row numbers.
You can use this approach to delete.
select
ROW_NUMBER() over (partition by Id
, StudentNo
order by Id, StudentNo, section)as Duplicate_RowNumber
, * into #tmp4 from #Temp1
--You can add section in your order as well for consistency purpose.
delete
from #tmp4
output deleted.id, deleted.Name, deleted.StudentNo, deleted.ClassCode,
deleted.Section into #Temp2
where Duplicate_RowNumber > 1
After that it seems like you want to keep one row in your final table and put the other one in you deleted table. For Elle it will delete one of the rows from Final table and keep only one since your partition is not based on section.
To make sure that you delete 1 row from your final table you can use this.
DELETE t
OUTPUT deleted.Id , deleted.Name ,deleted.StudentNo ,deleted.ClassCode
,deleted.Section into #inserted FROM
(select *, row_number() over (Partition by tm.name, tm.studentNo Order by ID,
StudentNo, section ) rownum from #temp1 tm) t
join #Temp2 t2 on t.Id = t2.Id
and t.Name = t2.Name
and t.StudentNo = t2.StudentNo
and t.ClassCode = t2.ClassCode
and t.Section = t2.Section
where t.rownum > 1
If you notice I added this row number, so that it will not two delete the rows from final table, since Joe and Harry has all the matching attributes, and it will delete two rows.
select * from #inserted
Output you get:
Id Name StudentNo ClassCode Section
3 Elle 121 J1 E1
2 Harry 113 X2 H
1 Joe 123 A1 I
Finally you can update final table in this way. #Scenario 2
update TMP
SET ClassCode = NULL, SECTION = NULL
FROM
#Temp1 TMP
JOIN #INSERTED I ON TMP.Id = I.Id
AND TMP.StudentNo = I.StudentNo
SELECT * FROM #Temp1
Final Output:
Id Name StudentNo ClassCode Section
1 Joe 123 NULL NULL
2 Harry 113 NULL NULL
3 Elle 121 NULL NULL
8 Jane 191 A1 E
5 Silva 811 S1 SE
6 Juan 411 S2 SE
7 Carla 431 S2 SE
Please note that I have added scripts and output only for the parts where it required change, and rest part is same script provided by you.
these are the 3 tables(sailors , boats , reserves) and i want to know the sailors who reserved both interlake boats(101,102) .The bID should be calculated by the computer
`select * from sailors
where sid in(select sid from reserves
inner join boats on reserves.bid = boats.bid
where boats.bname='Interlake')`
a link to see the image of tables https://i.stack.imgur.com/hfHpH.png
or link to pdf form http://www.cs.toronto.edu/~avaisman/csc343summer/tutorials/Tutorial_5.pdf
result = dustin luber horatio
expected = dustin horation
thanks for ur help
You can use the following
Explanation
Know the number of boats where name = interlake using cte report
Find all the sailors that reserved the boat with name interlake using summary cte
Find all sailors who reserved all the boats
Query
declare #boat varchar(50) = 'Interlake'
;with report as (
select bname,count(*) as [Count]
from boats b
where b.bname=#boat
group by b.bname
), summary as(
select s.sname, b.bname, count(*) as [Count]
from sailors s
inner join reserves r on r.sid = s.sid
inner join boats b on b.bid = r.bid
where b.bname=#boat
group by s.sname,b.bname
)
select s.*
from summary s
inner join report r on r.bname = s.bname and r.[Count] = s.[Count]
Here a working demo
Hope this will help you
The following SQL will build temp table variables, populate them and then use joins to show you which sailors have reserved the InterLake boats.
--Build sailors temp table variable and insert test data
declare #sailors table
([sid] int
,[name] varchar(50))
insert into #sailors
([sid],[name])
values
(1,'Sailor One'),
(2,'Sailor Two'),
(3,'Sailor Three')
--Build boats temp table variable and insert test data
declare #boats table
(bid int
,[name] varchar(50))
insert into #boats
(bid,[name])
values
(1,'10 foot John Boat'),
(2,'14 foot John Boat'),
(3,'18 foot Ski Boat'),
(4,'Interlake')
--Build reserves temp table variable and insert test data
declare #reserves table
(rid int
,[sid] int
,bid int)
insert into #reserves
(rid,[sid],bid)
values
(1, 1, 4),
(2, 3, 1),
(3, 2, 4)
--Query to select which sailors have reserved Interlake boats
select s.name as Sailor
,b.name as Boat
from #sailors s
inner join #reserves r on r.[sid] = s.[sid]
inner join #boats b on b.bid = r.bid
where b.name = 'InterLake'
--Results
Sailor Boat
Sailor One Interlake
Sailor Two Interlake
First we will join the two tables on where sailors sid matches reserves sid AND where the bid is 101 or 102, because that is what Interlake's bid is. If you want to do by where bname = interlake, you will join boats into reserves. This is something you have already done in your original query using inner join. I choose to use left:
select s.*
from sailors s
left join reserves r on r.sid = s.sid
where r.bid in (101, 102)
Now that I have everyone who has reserved the boats 101 or 102. I want to filter out those who only have both. In this case, I want to query from my filtered results. I decided to use a derived table, but you can use a cte alternatively.
Something like:
select *
from
(select s.*
from sailors s
left join reserves r on r.sid = s.sid
where r.bid in (101, 102)) t1 --t1 is the alias
But I don't want everything from this filtered table. I want the names.
select sname
from
(select s.*
from sailors s
left join reserves r on r.sid = s.sid
where r.bid in (101, 102)) t1
But I only want the names of those who have rented both 101 and 102. In this case if I count by the name, that means you could have only rented both if your count is GREATER than 1:
select sname --select the column of names I want
,count(sname) as total --here's the count of names, completely optional
from
(select s.*
from sailors s
left join reserves r on r.sid = s.sid
where r.bid in (101, 102)) t1
group by sname --anything you use an aggregation you'll need to likely group by something
having count(sname) > 1 --my filter after I group my names
Here is a rextester sample you can play with.
SELECT DISTINCT S.SID,S.SNAME,S.RATING,S.AGE
FROM SAILORS S,RESERVES R,BOATS B
WHERE S.SID=R.SID AND R.BID=B.BID
AND NOT EXISTS
((SELECT B.BID
FROM BOATS B WHERE B.BNAME NOT LIKE '%INTERLAKE%')
EXCEPT
(SELECT R.BID
FROM RESERVES R
WHERE R.SID=S.SID))
I have a hierarchical data structure stored using materialized paths.
Table:Files
node parentNode name path
100 NULL f1 /f1/
101 100 f2 /f1/f2/
102 101 f3 /f1/f2/f3/
I have the node column as primary key(clustered)
Now if I want to find the ancestors of f3, given the path, I do something like this:
SELECT * FROM Files WHERE '/f1/f2/f3/' LIKE [path] + '%'
The problem with this is, the execution plan uses a clustered index scan( which I think SQL server defaults to for table scans)
Is there anyway I can find the ancestors of a node, given the path in a more efficient manner, preferably not using a CTE? I also have a depth column at my disposal if required.
If you have slow moving hierarchies, consider adding Range Keys. They facilitate navigation, filtration, and/or aggregration without the need of recursion.
The Range keys indicate ownership between X and Y. The range keys are especially helpful when dealing with large hierarchies (180K nodes).
The following is a simplified example, but may help.
Sample Hier Build
--Drop Table #MyHier
Declare #YourTable table (id int,ParentId int,Name varchar(50))
Insert into #YourTable values
(11, NULL,'A')
,(12, 11 ,'B')
,(13, 12 ,'F')
,(14, 13 ,'C')
,(15, 13 ,'D')
,(16, 11 ,'E')
,(17, 12 ,'G')
,(18, NULL ,'M')
,(19, 18 ,'N')
,(20, 18 ,'O')
,(21, 20 ,'P')
Declare #Top int = null --<< Sets top of Hier Try 3
Declare #Nest varchar(25) = '|-----' --<< Optional: Added for readability
;with cteP as (
Select Seq = cast(10000+Row_Number() over (Order by Name) as varchar(500))
,ID
,ParentId
,Lvl=1
,Name
,Path = cast('/'+Name+'/' as varchar(500))
From #YourTable
Where IsNull(#Top,-1) = case when #Top is null then isnull(ParentId ,-1) else ID end
Union All
Select Seq = cast(concat(p.Seq,'.',10000+Row_Number() over (Order by r.Name)) as varchar(500))
,r.ID
,r.ParentId
,p.Lvl+1
,r.Name
,cast(p.path + '/'+r.Name+'/' as varchar(500))
From #YourTable r
Join cteP p on r.ParentId = p.ID)
,cteR1 as (Select *,R1=Row_Number() over (Order By Seq) From cteP)
,cteR2 as (Select A.ID,R2=Max(B.R1) From cteR1 A Join cteR1 B on (B.Seq like A.Seq+'%') Group By A.Seq,A.ID )
Select A.R1
,B.R2
,A.ID
,A.ParentId
,A.Lvl
,Name = Replicate(#Nest,A.Lvl-1) + A.Name
,Path
Into #MyHier
From cteR1 A
Join cteR2 B on A.ID=B.ID
Select Full Hier
-- Get The Full Hier
Select *
From #MyHier
Order By R1
Returns
Get Ancestors
-- Get Ancestors of a Node
Declare #GetAncestors int = 15
Select A.*
From #MyHier A
Join (Select R1 From #MyHier Where ID=#GetAncestors) B
on B.R1 between A.R1 and A.R2
Order By A.R1
Returns
Select Descendants
-- Get Descendants of a Node
Declare #GetDesendants int = 12
Select A.*
From #MyHier A
Join (Select R1,R2 From #MyHier Where ID=#GetDesendants) B
on A.R1 between B.R1 and B.R2
Order By A.R1
Returns
i have to show employee name and his manager name hierarchy separated by comma.
if ram is top manager and ram is sam manager and sam is rahim manager then i would like to have output like
Desired output
EMP Name Manager's Name
--------- ---------------
Rahim Sam,Ram
Sam Ram
Ram No manager
i got script which show the employee and his manager name. here is script
;WITH EmployeeCTE AS
(
Select ID,Name,MgrID, 0 as level FROM #Employee
WHERE ID=3
UNION ALL
Select r.ID,r.Name,r.MgrID, level+1 as level
FROM #Employee r
JOIN EmployeeCTE p on r.ID = p.MgrID
)
Select e1.Name
,ISNULL(e2.Name,'Top BOSS') as [Manager Name]
,row_number() over (order by e1.level desc) as [Level]
from EmployeeCTE e1
left join EmployeeCTE e2 on e1.MgrID=e2.ID
Output
Name Manager Name Level
Simon Top BOSS 1
Katie Simon 2
John Katie 3
i also know how to show comma separated list. here is one sample script.
SELECT
PNAME,
STUFF
(
(
SELECT ',' + Mname
FROM Myproducts M
WHERE M.PNAME = P.PNAME
ORDER BY Mname
FOR XML PATH('')
), 1, 1, ''
) AS Models
FROM
Myproducts p
GROUP BY PNAME
now some tell me how could i merge two script to get the desired output. thanks
CREATE TABLE #EMP (
EmpID INT
, ManagerID INT
, Name NVARCHAR(50) NULL
);
INSERT INTO #EMP (EmpID, ManagerID, Name)
VALUES
( 1, NULL, 'John')
, (2, 1, 'Katie')
, (3, 2, 'Simon');
SELECT *
FROM
#EMP;
WITH a AS (
SELECT
EmpID
, Name
, ManagerID
, CONVERT(NVARCHAR(MAX),'') AS ManagerChain
FROM
#Emp
WHERE
ManagerID IS NULL
UNION ALL
SELECT
e.EmpID
, e.Name
, e.ManagerID
, CASE
WHEN a.ManagerChain ='' THEN a.Name
ELSE CONCAT(a.Name, CONCAT(',',a.ManagerChain))
END
FROM
#Emp e
JOIN a ON e.ManagerID = a.EmpID
)
SELECT
a.Name
, IIF(a.ManagerChain='','No Manager',a.ManagerChain) AS ManagerChain
FROM
a;
DROP TABLE #EMP;
Assuming a table structure of
DECLARE #Employee TABLE(
ID INT,
Name VARCHAR(10),
MgrID INT)
INSERT INTO #Employee
VALUES (1,'Ram',NULL),
(2,'Sam',1),
(3,'Rahim',2);
You can use
WITH EmployeeCTE
AS (SELECT ID,
Name,
MgrID,
0 AS level,
CAST('No manager' AS VARCHAR(8000)) AS [Managers Name]
FROM #Employee
WHERE MgrID IS NULL
UNION ALL
SELECT r.ID,
r.Name,
r.MgrID,
level + 1 AS level,
CAST(P.Name + CASE
WHEN level > 0
THEN ',' + [Managers Name]
ELSE ''
END AS VARCHAR(8000))
FROM #Employee r
JOIN EmployeeCTE p
ON r.MgrID = p.ID)
SELECT *
FROM EmployeeCTE
I have two tables -
Table Records with columns ID, Code,ProviderId
Table Codes with columns Code, Number, ProviderId
Sample Data:
Table Records
ID Code ProviderId
1 ABC 1
2 DEF 2
3 XYZ 1
4 PQR 2
Table Codes
Code Number ProviderId
ABC 1111 1
Default 9999 1
XYZ 2222 2
Default 4444 2
All the rows in Records table will have a code. Codes table will have a set of codes defined with other information. It is not necessary that all the codes in Records table will have an entry in Codes table. For Code in Records table if corresponding value exists then select the same else need to select Default code based on the ProviderID column.
My expected result is:
ID Code Number
-------------------------
1 ABC 1111
2 DEF 9999 -> Picked up the default for ProviderId 1
3 XYZ 2222
4 PQR 4444 -. Picked up the default for ProviderId 2
I was able to achieve this using left outer join to add the records into a table variable and then again performing an inner join.
I was wondering if I can achieve this in a single select.
You can do this with left joins:
select r.id, r.code, coalesce(c.number, cd.number) as number:
from records r left join
codes c
on r.code = c.code left join
codes cd
on r.providerid = c.providerid and c.code = 'Default';
That is, lookup both values. Choose the one based on the code if there is a match; otherwise, use the default.
I think, your 'DEF' or 'PQR' entry is wrong, eigher one should have "Providerid" is 1. I prepare a sample from your data.
Use some code of #Gordon. but logic is different.
declare #records table(id int, code varchar(50), providerid int)
declare #code table(code varchar(50), number int, providerid int)
insert into #records values (1,'ABC',1), (2,'DEF',2),(3,'XYZ',1),(4,'PQR',2)
insert into #code values ('ABC',1111,1 ), ('DEFAULT',9999, 1) , ('XYZ',2222,2) , ('Default', 4444, 2)
--your data in wrongly entered , that why the 'DEF' show the wrong thing below.
select
id, r.code ,
case
when ISNULL(c.code , '') = ''
then c1.number
else c.number
end
from
#records r
left outer join #code c on r.code = c.code
left outer join #code c1 on r.providerid = c1.providerid and LOWER( c1.code) = 'default'
--if the default entry is multiple times, then good to use sub-query with top
select
id, r.code ,
case
when ISNULL(c.code , '') = ''
then ( select top 1 number from #code c1 where r.providerid = c1.providerid and LOWER( c1.code) = 'default' )
else c.number
end
from
#records r
left outer join #code c on r.code = c.code
Try this.. As mentioned by others 'PQR' entry is wrong in your OUTPUT. It should not be '9999'. It should be '4444'
SELECT a.ID,b.Code,b.Number
FROM Records a
JOIN code b
ON a.Code = b.Code
UNION ALL
SELECT a.ID,a.Code,b.Number
FROM Records a
JOIN code b
ON a.ProviderId = b.ProviderId
WHERE NOT EXISTS(SELECT 1
FROM Code aa
WHERE aa.Code = a.Code)
AND b.Code = 'default'
SELECT A.ID,A.Code,B.Number,A.ProviderId
FROM #Source A
JOIN #Code B ON A.Code = B.Code
UNION
(SELECT A.ID,A.Code,B.Number,A.ProviderId
FROM #Source A
JOIN #Code B ON A.Code != B.Code
AND B.Code = 'Default'
WHERE A.Code NOT IN (SELECT A.Code
FROM #Source A
JOIN #Code B ON A.Code = B.Code)
AND A.ProviderId = B.ProviderId )
Try using this