How to get the Managers active descendants count - sql

I need to create a query that display the Employee, Manager, Active, and the active descendants under the manager's hierarchy. Anyone can help me on this?
Results:
/* EmployeeId ManagerId IsActive NoofDescendantsActive
1 -1 0 4
2 1 1 3
3 1 0 2
4 2 1 2
6 2 0 0
7 3 1 0
8 3 1 0
9 4 1 1
10 9 1 0
11 9 0 0
*/
create table Person
(
EmployeeId int,
ManagerId int,
IsActive bit
);
insert into Person(EmployeeId,ManagerId,IsActive) values
(1, -1, 0),
(2, 1, 1),
(3, 1, 0),
(4, 2, 1),
(6, 2, 0),
(7, 3, 1),
(8, 3, 1),
(9, 4, 1),
(10, 9, 1),
(11, 9, 0);

The solution of Giorgos has a flaw. If employee #6 is made active, manager #1 should increase by 1 only. However, in his solution, it increased by 2.
EmployeeId NoofDescendantsActive
1 6
2 4
3 2
4 2
9 1
Here's a solution that works:
with Managers as
(
select
RootManagerId = EmployeeId, IsActive
from dbo.Person
where EmployeeId in (select ManagerId from dbo.Person where IsActive = 1)
),
A as -- Manager with employees
(
select m.RootManagerId, AnchorManagerId = m.RootManagerId,
ManagerId = convert(int, null)
from Managers m
union all
select m.RootManagerId, AnchorManagerId = d.EmployeeId,
d.ManagerId
from A m
-- collect descendants
join dbo.Person d on m.AnchorManagerId = d.ManagerId and d.IsActive = 1
),
Solution as
(
select RootManagerId, count(*) as Descendants
from A
where ManagerId is not null
group by RootManagerId
)
-- select * from A order by RootManagerId; -- Uncomment to see how things works
select * from Solution;
Result:
RootManagerId Descendants
1 4
2 3
3 2
4 2
9 1
Here's the result if employee #6 is made active:
RootManagerId Descendants
1 5
2 4
3 2
4 2
9 1

I think you can get the desired result using the following recursive CTE:
;WITH Descendants_CTE AS (
-- Anchor member: Get leaf nodes first
SELECT p1.EmployeeId, p1.ManagerId, p1.IsActive, 0 AS level
FROM Person AS p1
WHERE NOT EXISTS (SELECT 1
FROM Person AS p2
WHERE p2.ManagerId = p1.EmployeeId)
UNION ALL
-- Recursive member: Get nodes of next level, keep track of
-- the number of active nodes so far
SELECT p.EmployeeId, p.ManagerId, p.IsActive, level = level + 1
FROM Person AS p
INNER JOIN Descendants_CTE AS d ON p.EmployeeId = d.ManagerId
WHERE d.IsActive = 1
)
SELECT EmployeeId, SUM(level) AS NoofDescendantsActive
FROM Descendants_CTE
WHERE level > 0
GROUP BY EmployeeId
The recursion traverses the tree 'bottom-up', adding up the number of 'active' nodes met so far. If a non-active node is met traversal is terminated. Using a GROUP BY we can get the total number of active nodes beneath each non-leaf node. If you want to get the additional fields as well, then you have to JOIN the above query to the original table.
Hint: To better understand how the algorithm of the query actually works you can construct a graph of the tree on a piece of paper. Highlight each active node using a colored marker.
Demo here

Related

How to make new data is a function of last data

In SQL server I have a table just like the following table, original, and I want to update where Index ID>3
and the principle is lastaccmulated*2 + movement.
For example
where Index ID =3 accumulated = 8 * 2 + 2 =18
I tried the lag function but it can only be used in select, which means I cannot finish in one update.
Is there any sharp function to make this happen?
Table orginal
IndexID
accumulated
movement
1
5
2
2
8
2
3
0
2
4
0
2
5
0
2
Table what I want after update
IndexID
accumulated
movement
1
5
2
2
8
2
3
18
2
4
38
2
5
78
2
Just like above mention, it went wrong when I use lag function.
Try this:
DROP TABLE IF EXISTS #YOUR_TABLE
SELECT
id,
accumulated,
movement
INTO #YOUR_TABLE
FROM (
VALUES
(1, 5, 2),
(2, 8, 2),
(3, 0, 2),
(4, 0, 2),
(5, 0, 2),
(6, 0, 2)
) src (id, accumulated, movement)
;WITH
CALCULATION AS (
SELECT
id,
2 * accumulated + movement as accumulated
FROM #YOUR_TABLE
WHERE id = 2
UNION ALL
SELECT
yt.id,
2 * c.accumulated + yt.movement as accumulated
FROM CALCULATION c
JOIN #YOUR_TABLE yt ON yt.id = c.id + 1
)
UPDATE yt SET
yt.accumulated = c.accumulated
FROM #YOUR_TABLE yt
JOIN CALCULATION c ON
c.id = yt.id
WHERE
yt.id >= 3
OPTION (MAXRECURSION 0) -- To prevent recursion limitiations
SELECT * FROM #YOUR_TABLE
We are using recursive CTE here. Before UNION ALL we give values for step zero, after we have calculation based on previous step (yt.id = c.id + 1).

sql grouping grades

I have a table for subjects as follows:
id Subject Grade Ext
100 Math 6 +
100 Science 4 -
100 Hist 3
100 Geo 2 +
100 CompSi 1
I am expecting output per student in a class(id = 100) as follows:
Grade Ext StudentGrade
6 + 1
6 0
6 - 0
5 + 0
5 0
5 - 0
4 + 0
4 0
4 - 1
3 + 0
3 1
3 - 0
2 + 1
2 0
2 - 0
1 + 0
1 1
1 - 0
I would want this done on oracle/sql rather than UI. Any inputs please.
You should generate rows first, before join them with your table like below. I use the with clause here to generate the 18 rows in your sample.
with rws (grade, ext) as (
select ceil(level/3), decode(mod(level, 3), 0, '+', 1, '-', null)
from dual
connect by level <= 3 * 6
)
select r.grade, r.ext, nvl2(t.Ext, 1, 0) studentGrade
from rws r
left join your_table t
on t.Grade = r.Grade and decode(t.Ext, r.Ext, 1, 0) = 1
order by 1 desc, decode(r.ext, null, 2, '-', 3, '+', 1)
You could do something like this. In the WITH clause I generate two small "helper" tables (really, inline views) for grades from 1 to 6 and for "extensions" of +, null and -. In the "extensions" view I also create an "ordering" column to use in ordering the final output (if you are wondering why I included that).
Also in the WITH clause I included sample data - you will have to remove that and instead use your actual table name in the main query.
The idea is to cross-join "grades" and "extensions", and left-outer-join the result to your input data. Count the grades from the input data, grouped by grade and extension, and after filtering the desired id. The decode thing in the join condition is needed because for extension we want to treat null as equal to null - something that decode does nicely.
with
sample_inputs (id, subject, grade, ext) as (
select 100, 'Math' , 6, '+' from dual union all
select 100, 'Science', 4, '-' from dual union all
select 100, 'Hist' , 3, null from dual union all
select 100, 'Geo' , 2, '+' from dual union all
select 100, 'CompSi' , 1, null from dual
)
, g (grade) as (select level from dual connect by level <= 6)
, e (ord, ext) as (
select 1, '+' from dual union all
select 2, null from dual union all
select 3, '-' from dual
)
select g.grade, e.ext, count(t.grade) as studentgrade
from g cross join e left outer join sample_inputs t
on t.grade = g.grade and decode(t.ext, e.ext, 0) = 0
and t.id = 100 -- change this as needed!
group by g.grade, e.ext, e.ord
order by g.grade desc, e.ord
;
OUTPUT:
GRADE EXT STUDENTGRADE
----- --- ------------
6 + 1
6 0
6 - 0
5 + 0
5 0
5 - 0
4 + 0
4 0
4 - 1
3 + 0
3 1
3 - 0
2 + 1
2 0
2 - 0
1 + 0
1 1
1 - 0
It looks like you want sparse data to be filled in as part of joining students and subjects.
Since Oracle 10g the correct way to do this has been with a "partition outer join".
The documentation has examples.
https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/SELECT.html#GUID-CFA006CA-6FF1-4972-821E-6996142A51C6

How to get a Parent-Child record from different level in a same table in SSIS?

Below are the details and output required.
The table has 3 columns:
Record
Parent Record
isactive
Output required as below based on inactive column:
e.g 1
Record Parent_Record Isactive
1 0 1
2 1 0
3 1 0
4 2 0
5 3 1
output
Record Parent_Record Isactive
5 1 1
e.g 2
Record Parent_Record Isactive
1 0 0
2 1 0
3 1 1
4 2 0
5 3 1
output
Record Parent_Record Isactive
5 3 1
You can use a recursive CTE to build the hierarchy starting from the leaf node going back to the higher active parent:
declare #tmp table (Record int, Parent_Record int, Isactive bit)
declare #recordToCheck int = 5
insert into #tmp values
(1, 0, 1)
,(2, 1, 0)
,(3, 1, 0)
,(4, 2, 0)
,(5, 3, 1)
;WITH RESULT (Record, Parent_Record, Isactive, Lev)
AS
(
--anchor
SELECT A.Record,Parent_Record, Isactive, 1 AS LEVEL
FROM #tmp AS A
WHERE A.Record = #recordToCheck
UNION ALL
--outer
SELECT C.Record, C.Parent_Record, C.Isactive, Lev + 1
FROM #tmp AS C
INNER JOIN RESULT AS B
ON C.Record=B.Parent_Record
)
select top 1 #recordToCheck as Record, Record as Parent_Record, Isactive
from RESULT
where Isactive = 1
order by lev desc
Result for e.g 1:
Result for e.g 2:

Calculate loads and avoiding cursors

Given the following table structure, which is a representation of a bus route where passengers get on and off the bus with a door sensor. And, there is a person who sits on that bus with a clipboard holding a spot count.
CREATE TABLE BusLoad(
ROUTE CHAR(4) NOT NULL,
StopNumber INT NOT NULL,
ONS INT,
OFFS INT,
SPOT_CHECK INT)
go
INSERT BusLoad VALUES('AAAA', 1, 5, 0, null)
INSERT BusLoad VALUES('AAAA', 2, 0, 0, null)
INSERT BusLoad VALUES('AAAA', 3, 2, 1, null)
INSERT BusLoad VALUES('AAAA', 4, 6, 3, 8)
INSERT BusLoad VALUES('AAAA', 5, 1, 0, null)
INSERT BusLoad VALUES('AAAA', 6, 0, 1, 7)
INSERT BusLoad VALUES('AAAA', 7, 0, 3, null)
I want to add a column "LOAD" to this table that calculates the load at each stop.
Load = Previous stops load + current stop ONS - Current stop's OFFS if
SPOT_CHECK is null, otherwise LOAD = SPOT_CHECK
Expected Results:
ROUTE StopNumber ONS OFFS SPOT_CHECK LOAD
AAAA 1 5 0 NULL 5
AAAA 2 0 0 NULL 5
AAAA 3 2 1 NULL 6
AAAA 4 6 3 8 8
AAAA 5 1 0 NULL 9
AAAA 6 0 1 7 7
AAAA 7 0 3 NULL 4
I can do this with a cursor, but is there a way to do it using a query?
You can use the following query:
select ROUTE, StopNumber, ONS, OFFS, SPOT_CHECK,
COALESCE(SPOT_CHECK, ONS - OFFS) AS ld,
SUM(CASE WHEN SPOT_CHECK IS NULL THEN 0 ELSE 1 END)
OVER (PARTITION BY ROUTE ORDER BY StopNumber) AS grp
from BusLoad
to get:
ROUTE StopNumber ONS OFFS SPOT_CHECK ld grp
----------------------------------------------------
AAAA 1 5 0 NULL 5 0
AAAA 2 0 0 NULL 0 0
AAAA 3 2 1 NULL 1 0
AAAA 4 6 3 8 8 1
AAAA 5 1 0 NULL 1 1
AAAA 6 0 1 7 7 2
AAAA 7 0 3 NULL -3 2
All you want now is the running total of ld over ROUTE, grp partitions of data:
;WITH CTE AS (
....
previous query here
)
select ROUTE, StopNumber, ONS, OFFS, SPOT_CHECK, grp,
sum(ld) over (PARTITION BY ROUTE, grp ORDER BY StopNumber) as load
from cte
Demo here
Note: The above query works for versions starting from 2012. If you want a query for 2008 you have to somehow simulate sum() over (order by ...). You can find many relevant posts here in SO.
You may use recursive query
with act_load as
(
select *, ons load
from busload
where stopnumber = 1 and route = 'AAAA'
union all
select b.*, case when b.spot_check is null then l.load + b.ons - b.offs
else b.spot_check
end load
from busload b
join act_load l on b.StopNumber = l.StopNumber + 1 and
b.route = l.route
)
select *
from act_load
dbfiddle demo

Soccer league table standings with SQL

Perhaps a familiar table for many people. A soccer league table.
But, in this list there is one mistake, rank 4 and 5, are totally equal, so these teams should not be ranked 4 and 5, but 4 and 4, and then the ranking should continue with 6.
Ranking | Team | Points | Goals difference | Goals scored | Goals against
1 A 3 4 4 0
2 B 3 3 3 0
3 C 3 1 2 1
4 D 3 1 1 0
5 E 3 1 1 0
6 F 1 0 2 2
7 G 1 0 0 0
I have been trying to improve the MS SQL query that produces this table, by using a Common Table Expression and SELECT ROW_Number, but that never gives me the right result. Does anyone have a better idea?
You can do this easy by using the RANK() function.
declare #table as table
(
Team varchar(1),
Points int,
GoalsScored int,
GoalsAgainst int
)
insert into #table values ('A', 3, 4, 0),
('B', 3, 3, 0),
('C', 3, 2, 1),
('D', 3, 1, 0),
('E', 3, 1, 0),
('F', 1, 2, 2),
('G', 1, 0, 0)
select RANK() OVER (ORDER BY points desc, GoalsScored - GoalsAgainst desc, GoalsScored desc) AS Rank
,team
,points
,GoalsScored - GoalsAgainst as GoalsDifference
,GoalsScored
,GoalsAgainst
from #table
order by rank
Here is a possible solution. I'm not sure specifically how you are ranking so I've ranked based on Points DESC, Goals Diff DESC, Goals Scored DESC and Goals Against ASC.
;WITH
src AS (
SELECT Team, Points, GoalsDiff, GoalsScor, GoalsAga
FROM dbo.[stats]
)
,src2 AS (
SELECT Points, GoalsDiff, GoalsScor, GoalsAga
FROM src
GROUP BY Points, GoalsDiff, GoalsScor, GoalsAga
)
,src3 AS (
SELECT ROW_NUMBER() OVER (ORDER BY Points DESC, GoalsDiff DESC, GoalsScor DESC, GoalsAga) AS Ranking
,Points, GoalsDiff, GoalsScor, GoalsAga
FROM src2
)
SELECT src3.Ranking, src.Team, src.Points, src.GoalsDiff, src.GoalsScor, src.GoalsAga
FROM src
INNER JOIN src3
ON src.Points = src3.Points
AND src.GoalsDiff = src3.GoalsDiff
AND src.GoalsScor = src3.GoalsScor
AND src.GoalsAga = src3.GoalsAga
The basic approach I used is to select just the stats themselves then group them all. Once grouped then you can rank them and then join the grouped stats with ranking back to the original data to get your rank numbers against the teams. One way to think of it is that you are ranking the stats not the teams.
Hope this helps.