Get all subordinates based on line manager marker - sql

I have an employees table that holds an employeeID, Name...etc.... and intLineManager. intLine manager is the employeeID of your line manager. Something like this, with Dave being the top boss with no manager.
intEmpID |Name|intLineManager
1 |Dave| NULL
2 |Sue |1
3 |Mike|1
4 |Matt|2
5 |Sara|3
6 |Paul|4
I'm looking for an SQL script that will return all employees that are underneath a person.
So a search on Mike will return Sara, a search on Sue will return Matt + Paul and finally a search on Dave will return everyone.

You can use a recursive CTE like so:
CREATE TABLE #Employees(EmpID int, Name varchar(10), LineManager int NULL)
INSERT INTO #Employees VALUES
(1, 'Dave', NULL),
(2, 'Sue', 1),
(3, 'Mike', 1),
(4, 'Matt', 2),
(5, 'Sara', 3),
(6, 'Paul', 4)
;WITH CTE as
(
SELECT EmpID, Name, LineManager
FROM #Employees
WHERE EmpID = 2 --Sue
UNION ALL
SELECT e.EmpID, e.Name, e.LineManager
FROM #Employees AS E
INNER JOIN CTE AS C ON c.EmpID = e.LineManager
)
SELECT *
FROM CTE
where EmpID > (SELECT TOP 1 EmpID FROM CTE)
ORDER BY EmpID
Result for sue(EmpID 2)
EmpID Name LineManager
----------- ---------- -----------
4 Matt 2
6 Paul 4

Related

How to aggregate and count avg value by groups from 2 tables in sql?

I have 2 tables , department and employee. I need to get department name and average age of it.
create table Dep(name_dep char, id_dep int);
insert into Dep values("econ", 1);
insert into Dep values("credit", 2);
insert into Dep values("energy", 3);
insert into Dep values("retail", 4);
insert into Dep values("manufactury", 5);
create table Emp(id_emp int, id_dep int, age int, person_name char, salary int );
insert into Emp values(1, 1, 23, 'john', 200);
insert into Emp values(3, 2, 3, 'dalbai', 100);
insert into Emp values(6, 3, 53, 'borat', 300);
insert into Emp values(7, 1, 63, 'erjan', 1600);
insert into Emp values(9, 2, 73, 'sergey', 1000);
insert into Emp values(8, 5, 83, 'lucy', 20);
insert into Emp values(90, 4, 93, 'mike', 1200);
How to select dept name and avg age of employees in that dept?
SELECT name_dep, average_age
FROM Emp e
INNER JOIN
(
SELECT name_dep, AVG(age) AS average_age
FROM Dep d
GROUP BY id_dep
) d
ON e.id_dep = d.id_dep
YOu can try below -
DEMO
select name_dep,avg(age)
from emp a inner join dep b on a.id_dep=b.id_dep
group by a.id_dep,name_dep
Join the tables, group by id_dep, name_dep and then get the average age:
select
d.name_dep, avg(age) average_age
from Dep d inner join Emp e
on e.id_dep = d.id_dep
group by d.id_dep, d.name_dep
See the demo.
Results:
| name_dep | average_age |
| ----------- | ----------- |
| econ | 43 |
| credit | 38 |
| energy | 53 |
| retail | 93 |
| manufactury | 83 |
you can do something like
SELECT
d.name_dep, AVG(age) as avg_age
FROM
Emp e, Dep d
WHERE
e.id_dep = d.id_dep
GROUP BY
d.name_dep
SELECT d.name_dep,AVG(e.age)
FROM Dep d JOIN Emp e ON d.id_dep = e.id_dep
GROUP BY d.name_dep;

Put value in minimum ID record in SQL data set

I have a table where I'm trying to get the following result: CategoryType should be set to 0 for the member record with the lowest categoryID and 1 if that ID is NULL. CategoryType is not a field from the table so it should be added via a Case statement. I wrote a script using the Row_Number() window function to organize the records but I'm not sure how to put a 0 for the lowest record. I tried using a Min(Case) statement but that didn't work.
declare #t table(memberid int, lastname varchar(50), firstname varchar(50), categoryid int)
insert into #t
values(1, 'Jones', 'Tom', 2), (1, 'Jones', 'Tom', 4),
(2, 'Hanson', 'Ron', 3), (2, 'Hanson', 'Ron', 4),
(2, 'Hanson', 'Ron', 5),
(3, 'Smith', 'Jack', NULL);
This is the result I'm trying to get:
MemberID LastName FirstName CategoryID CategoryType
1 Jones Tom 2 0
1 Jones Tom 4 NULL
2 Hanson Ron 3 0
2 Hanson Ron 4 NULL
2 Hanson Ron 5 NULL
3 Smith Jack NULL 1
Or this:
select *
,case
when categoryid is null then 1
when FIRST_VALUE(categoryid) over (partition by memberid order by categoryid) = categoryid then 0 else null
end as x
from #t
I think you want this
SELECT *,CASE WHEN categoryid IS NULL THEN 1
WHEN ROW_NUMBER() OVER (PARTITION BY categoryid ORDER BY memberid) = 1
THEN 0
END CategoryType FROM #T
Easier to read/extend (if needed):
SELECT a.MemberID,a.LastName,a.FirstName,a.CategoryID
,CASE
WHEN a.rn = 1 AND a.CategoryID IS NULL THEN 1
WHEN a.rn = 1 THEN 0
ELSE NULL
END AS [CategoryType]
FROM (
SELECT t.MemberID,t.LastName,t.FirstName,t.CategoryID
,ROW_NUMBER()OVER(PARTITION BY t.MemberID ORDER BY t.CategoryID) AS [rn]
FROM #t t
) a
;
I think you want:
select t.*,
(case when categoryid is null
then 1
when row_number() over (partition by memberid order by categoryid) = 1
then 0
end) as categorytype
from #t t;
Here is a db<>fiddle.
Hope Can Help You
declare #t table(memberid int,
lastname varchar(50),
firstname varchar(50),
categoryid int)
insert into #t
values(1, 'Jones', 'Tom', 2),
(1, 'Jones', 'Tom', 4),
(2, 'Hanson', 'Ron', 3),
(2, 'Hanson', 'Ron', 4),
(2, 'Hanson', 'Ron', 5),
(3, 'Smith', 'Jack', NULL)
Select
A.*,
Case When a.categoryid is null Then '1'
When b.categoryid is not null Then '0' Else null End CategoryType
From #t A
Left Join (
Select memberid, min(categoryid) categoryid From #t
Group By memberid
) B On B.memberid = A.memberid and B.categoryid = A.categoryid

Recursive CTE not getting desired result. How to assign anchor only?

The application is for an employee id get all managers
declare #emp table (id int primary key, mgr int);
insert into #emp values
(1, null)
, (2, 1)
, (3, 2)
, (4, null)
, (5, 4);
select * from #emp;
; with cte as
( select e.id, e.mgr, cnt = 1
from #emp e
union all
select e.id, e.mgr, cnt + 1
from #emp e
join cte
on cte.mgr = e.id
)
select id, mgr, cnt
from cte
where id = 3;
The above only returns the single row for id = 3. I get that is expected but not what I desire. I want to start (anchor) at 3 and get the manager chain.
If I hard code the anchor I get the desired result.
See below:
; with cte as
( select e.id, e.mgr, cnt = 1
from #emp e
where e.id = 3
union all
select e.id, e.mgr, cnt + 1
from #emp e
join cte
on cte.mgr = e.id
)
select id, mgr, cnt
from cte;
My question is how to assign the anchor (top part) only in a where on the cte?
If not in the where is there another way to assign the anchor only (not hard coding in the cte)?
You need to keep the starting position in your cte:
declare #emp table (id int primary key, mgr int);
insert into #emp values
(1, null)
, (2, 1)
, (3, 2)
, (4, null)
, (5, 4);
; with cte as
( select e.id ori, e.id, e.mgr, cnt = 1
from #emp e
union all
select cte.ori, e.id, e.mgr, cnt + 1
from #emp e
join cte
on cte.mgr = e.id
)
select ori, id, mgr, cnt
from cte
where cte.ori = 3;
Result:
+-----+----+------+-----+
| ori | id | mgr | cnt |
+-----+----+------+-----+
| 3 | 3 | 2 | 1 |
| 3 | 2 | 1 | 2 |
| 3 | 1 | NULL | 3 |
+-----+----+------+-----+
I made just one change and removed (seemingly) unnecessary column:
declare #emp table (id int primary key, mgr int);
insert into #emp values
(1, null)
, (2, 1)
, (3, 2)
, (4, null)
, (5, 4);
select * from #emp;
; with cte as
( select e.id, e.mgr
from #emp e
union all
select cte.id, e.mgr
from #emp e
join cte
on cte.mgr = e.id
)
select id, mgr
from cte
where id = 3;
Result is:
id | mgr
3 | 2
3 | 1
3 | NULL
You must decide whether to
start with a root node (add WHERE mgr IS NULL or an explicit id to your anchor) and move down the chain, or to
start with any child node (add where not exists(SELECT 1 FROM #emp AS x WHERE e.id=x.mgr) to the anchor) and move up or to
start with an explicit child (add where e.id=3 to the anchor) and move up.
The general advise is: Start with the narrowest anchor-set possible!
The first query you state in your question (as well as other answers here) will do a huge overdose creating each an any chain starting from everywhere with overlapping results.
As the recursive CTE is a hidden RBAR the engine has no chance to predict its result and will create the full load - just to throw most of this away.
If this is a 1:n relation (always 1 mgr on-top) moving upwards will be much faster. Starting from a given child node You have exactly one step per level - that's it.
Applying the filter in a WHERE after the recursice CTE was worked out, means to create any possible chain just to throw most of them away...
Your second approach is the best I can think of actually.
So the question is: Why do you want to apply this filter at the end?

SQL Query Peers and above

There are recursive queries available to find all children or parents in a hierarchy (for example in an employee-manager hierarchy) but I couldn't find any example of peers and above/parents
EmpID FName LName MgrID
11 Sally Smith NULL
1 Alex Adams 11
2 Barry Brown 11
3 Lee Osaka 11
4 David Kennson 11
5 Eric Bender 11
7 David Lonning 11
6 Lisa Kendall 4
8 John Marshbank 4
12 Barbara ONeill 4
13 Phil Wilconkinski 4
9 James Newton 3
10 Terry OHaire 3
So if I want peer-above of EmpID=6 then it should return
EmpID FName LName MgrID
11 Sally Smith NULL grandparent/peer
1 Alex Adams 11 parent-peer
2 Barry Brown 11 parent-peer
3 Lee Osaka 11 parent-peer
4 David Kennson 11 (parent of id=6)
5 Eric Bender 11 parent-peer
7 David Lonning 11 parent-peer
6 Lisa Kendall 4 (id=6)
8 John Marshbank 4 peer
12 Barbara ONeill 4 peer
13 Phil Wilconkinski 4 peer
Any help will be very appreciated
With the way your table is currently structured, one way of achieving this is to include a path in your recursive CTE, then parse the path to figure out whether someone belongs to that path in the hierarchy.
For example,
DECLARE #Table TABLE
(
EmpID INT NOT NULL,
FName VARCHAR(20) NOT NULL,
LName VARCHAR(20) NOT NULL,
MgrID INT
);
INSERT #Table (EmpID, FName, LName, MgrID)
VALUES
(11, 'Sally', 'Smith', NULL),
(1, 'Alex', 'Adams', 11),
(2, 'Barry', 'Brown', 11),
(3, 'Lee', 'Osaka', 11),
(4, 'David', 'Kennson', 11),
(5, 'Eric', 'Bender', 11),
(7, 'David', 'Lonning', 11),
(6, 'Lisa', 'Kendall', 4),
(8, 'John', 'Marshbank', 4),
(12, 'Barbara', 'ONeill', 4),
(13, 'Phil', 'Wilconkinski', 4),
(9, 'James', 'Newton', 3),
(10, 'Terry', 'OHaire', 3);
WITH CTE AS
(
SELECT EmpID,
FName,
LName,
MgrID,
Tree = CAST(EmpID AS VARCHAR(255)),
Lvl = 1
FROM #Table
WHERE MgrID IS NULL
UNION ALL
SELECT T.EmpID,
T.FName,
T.LName,
T.MgrID,
CAST(CTE.Tree + '-' + CAST(T.EmpID AS VARCHAR(255)) AS VARCHAR(255)), -- Your paths will look like '11-4-6'.
CTE.Lvl + 1
FROM CTE
JOIN #Table T ON T.MgrID = CTE.EmpID
)
SELECT EmpID,
FName,
LName,
MgrID,
Lvl,
Direct = CASE MIN(CASE WHEN EmpID = Item THEN 1 ELSE 2 END) WHEN 1 THEN 'Direct' ELSE 'Peer' END
FROM CTE
JOIN
(
SELECT Item
FROM CTE
CROSS APPLY dbo.[DelimitedSplit8K](Tree, '-') D
WHERE CTE.EmpID = 6
) D ON D.Item = CTE.EmpID
OR D.Item = CTE.MgrID
GROUP BY EmpID, FName, LName, MgrID, Lvl;
This uses a split function to break up the path and only display people in that path. The split function can be found here and looks like this:
CREATE FUNCTION [dbo].[DelimitedSplit8K]
--===== Define I/O parameters
(#pString VARCHAR(8000), #pDelimiter CHAR(1))
--WARNING!!! DO NOT USE MAX DATA-TYPES HERE! IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
-- enough to cover VARCHAR(8000)
WITH E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(#pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT 1 UNION ALL
SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(#pString,t.N,1) = #pDelimiter
),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(#pDelimiter,#pString,s.N1),0)-s.N1,8000)
FROM cteStart s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
Item = SUBSTRING(#pString, l.N1, l.L1)
FROM cteLen l
;

Display order of a SQL Query without order by clause

I am writing a simple query in SQL Server:
Select
EmpId, EmpName, Sal
from
Emp
where
EmpId in (10,9,5,7,3,8);
I want to get the output in a same order which is given i.e; 10,9,5,7,3,8
Actually whatever I'll give the result will display with given order without
order by ascending or descending.
How can I do that? Please help.
No way to do this natively.
Try:
SELECT EmpId,EmpName,Sal
FROM Emp
WHERE EmpId IN (10,9,5,7,3,8)
ORDER BY CASE EmpId
WHEN 10 THEN 1
WHEN 9 THEN 2
WHEN 5 THEN 3
WHEN 7 THEN 4
WHEN 3 THEN 5
WHEN 8 THEN 6
ELSE 7
END;
You can use a table variable to pass the inputs. You must insert the records into this table variable in the desired order.
Declare #empids table(id int identity(1,1),empid int)
insert into #empids values(10),(9),(5),(7),(3),(8)
Select e.EmpId,e.empname,e.sal from Emp e
join #empids t on e.EmpId = t.empid
order by t.id
Try this.
You can use CHARINDEX function in an odd way: search for the id in the comma separated list and order the result by the position.
Consider this list for example 10,9,5,7,3,8... the substring 10 appears at 1st position while 9 appears at 4th. Just order by the substring position.
CREATE TABLE Emp
(EmpId int, EmpName varchar(100), Sal int)
;
INSERT INTO Emp
(EmpId, EmpName, Sal)
VALUES
(1, 'John', NULL),
(2, 'Jane', NULL),
(3, 'Smith', NULL),
(4, 'Doe', NULL),
(5, 'Ben', NULL),
(6, 'Steve', NULL),
(7, 'Andrew', NULL),
(8, 'Simon', NULL),
(9, 'Jack', NULL),
(10, 'Allen', NULL)
;
SELECT
EmpId, EmpName, Sal
FROM
Emp
WHERE
EmpId in (10,9,5,7,3,8)
ORDER BY
CHARINDEX(CONCAT(',', EmpId, ','), CONCAT(',', '10,9,5,7,3,8', ','))
;
Result:
EmpId | EmpName | Sal
------+---------+-----
10 | Allen | NULL
9 | Jack | NULL
5 | Ben | NULL
7 | Andrew | NULL
3 | Smith | NULL
8 | Simon | NULL
You can do this dynamically if your list is a comma-delimited-string. First, you must have a splitter function. Here is the DelimitedSplit8k written by Jeff Moden:
CREATE FUNCTION [dbo].[DelimitedSplit8K](
#pString VARCHAR(8000), #pDelimiter CHAR(1)
)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
WITH E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
)
,E2(N) AS (SELECT 1 FROM E1 a, E1 b)
,E4(N) AS (SELECT 1 FROM E2 a, E2 b)
,cteTally(N) AS(
SELECT TOP (ISNULL(DATALENGTH(#pString), 0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
)
,cteStart(N1) AS(
SELECT 1 UNION ALL
SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(#pString, t.N, 1) = #pDelimiter
),
cteLen(N1, L1) AS(
SELECT
s.N1,
ISNULL(NULLIF(CHARINDEX(#pDelimiter, #pString, s.N1),0) - s.N1, 8000)
FROM cteStart s
)
SELECT
ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
Item = SUBSTRING(#pString, l.N1, l.L1)
FROM cteLen l
Then, you declare the list of empIds as csv string and use the splitter:
DECLARE #empIds VARCHAR(MAX) = '10,9,5,7,3,8';
SELECT e.EmpId, e.EmpName, e.Sal
FROM Emp e
INNER JOIN dbo.DelimitedSplit8K(#empIds, ',') s
ON s.Item = l.EmpId
ORDER BY s.ItemNumber
Following query will give you the exact result (without order by):
SELECT EmpId,EmpName,Sal
FROM Emp
WHERE EmpId IN (10)
union all
SELECT EmpId,EmpName,Sal
FROM Emp
WHERE EmpId IN (9)
union all
SELECT EmpId,EmpName,Sal
FROM Emp
WHERE EmpId IN (5)
union all
SELECT EmpId,EmpName,Sal
FROM Emp
WHERE EmpId IN (7)
union all
SELECT EmpId,EmpName,Sal
FROM Emp
WHERE EmpId IN (3)
union all
SELECT EmpId,EmpName,Sal
FROM Emp
WHERE EmpId IN (8)
If Ids is a variable passed as input parameter to a stored procedure, you can split it using CTE.
--#inputIDs VARCHAR(300) => '10,9,5,7,3,8'
;WITH MyIds AS
(
SELECT 1 AS Position, CONVERT(INT, LEFT(#inputIds, CHARINDEX(',', #inputIDs)-1)) AS MyId,
RIGHT(#inputIds, LEN(#inputIds) - CHARINDEX(',', #inputIDs)) AS Remainder
WHERE CHARINDEX(',', #inputIDs)>0
UNION ALL
SELECT Position +1 AS Position, CONVERT(INT, LEFT(Remainder, CHARINDEX(',', Remainder)-1)) AS MyId,
RIGHT(Remainder, LEN(Remainder) - CHARINDEX(',', Remainder)) AS Remainder
FROM MyIds
WHERE CHARINDEX(',', Remainder)>0
UNION ALL
SELECT Position +1 AS Position, CONVERT(INT, Remainder) AS MyId,
NULL AS Remainder
FROM MyIds
WHERE CHARINDEX(',', Remainder)=0
)
SELECT e.EmpId, e.EmpName, e.Sal
FROM Emp AS e INNER JOIN MyIds AS a ON e.EmpId = a.MyId
ORDER BY a.Position
The simplest way to achieve this is probably to use a table-valued constructor as a table expression containing the ids with a sort value, then order on this value:
Select
Emp.EmpId, Emp.EmpName, Emp.Sal
from
Emp
Inner Join
(
Values
(10, 1),
(9, 2),
(5, 3),
(7, 4),
(3, 5),
(8, 6)
)
EmpIds (EmpId, Sort)
On
Emp.EmpId = EmpIds.EmpId
Order By
EmpIds.Sort Asc