Counting the hierarchy - sql

I have a table with a hierarchy:
CREATE TABLE mng
(
id INTEGER NOT NULL PRIMARY KEY,
name VARCHAR(50) NOT NULL,
manager_id INTEGER
);
INSERT INTO mng (id, name, manager_id) VALUES (1, 'Lola', NULL);
INSERT INTO mng (id, name, manager_id) VALUES (2, 'Bella', NULL);
INSERT INTO mng (id, name, manager_id) VALUES (3, 'Lo', 1);
INSERT INTO mng (id, name, manager_id) VALUES (4, 'Ann', 2);
INSERT INTO mng (id, name, manager_id) VALUES (5, 'Ki', 3);
INSERT INTO mng (id, name, manager_id) VALUES (6, 'Qo', 5);
I need to print all top managers (where manager_id is NULL) with their subordinates count (including all levels)
id | cnt
--------+---------
1 | 3
2 | 1
upd
was trying something like this:
WITH DirectReports(ManagerID, Employee) AS
(
SELECT id, 0 AS Employee
FROM mng
WHERE manager_id IS NULL
UNION ALL
SELECT e.id, Employee + 1
FROM mng AS e
INNER JOIN DirectReports AS d
ON e.manager_id = d.ManagerID
)
SELECT ManagerID, Employee
FROM DirectReports
ORDER BY ManagerID;
got level number, but how to get count?

The issue with your original code is you lose track of the "top manager". You can, however, retain this within the CTE, and simply have it listed for every employee:
WITH DirectReports(id, TopMgr, ManagerID, Employee) AS
(
SELECT id, id, NULL, 0 AS Employee
FROM mng
WHERE manager_id IS NULL
UNION ALL
SELECT e.id, d.TopMgr, e.manager_id, 1 AS Employee
FROM mng AS e
INNER JOIN DirectReports AS d
ON e.manager_id = d.id
)
SELECT TopMgr, SUM(Employee)
FROM DirectReports
GROUP BY TopMgr
ORDER BY TopMgr
Output:
TopMgr EmployeeCount
1 3
2 1

First you can get the employees under manager, top level manager. Later, you can count the employees under top level manager.
;WITH DirectReports(EmployeeId, ManagerID, TopMostManagerId) AS
(
SELECT id, Null, id
FROM #mng
WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.manager_Id, d.TopMostManagerId
FROM #mng AS e
INNER JOIN DirectReports AS d
ON e.manager_id = d.EmployeeId
)
SELECT TopMostManagerId, COUNT(EmployeeId) as CountOfEmployees
FROM DirectReports
WHERE ManagerID is not null -- excludes toplevel manager
group by TopMostManagerId
TopMostManagerId
CountOfEmployees
1
3
2
1

Related

Union two queries ordered by newid

I have a table that stores employees (id, name, and gender). I need to randomly get two men and two women.
CREATE TABLE employees
(
id INT,
name VARCHAR (10),
gender VARCHAR (1),
);
INSERT INTO employees VALUES (1, 'Mary', 'F');
INSERT INTO employees VALUES (2, 'Jake', 'M');
INSERT INTO employees VALUES (3, 'Ryan', 'M');
INSERT INTO employees VALUES (4, 'Lola', 'F');
INSERT INTO employees VALUES (5, 'Dina', 'F');
INSERT INTO employees VALUES (6, 'Paul', 'M');
INSERT INTO employees VALUES (7, 'Tina', 'F');
INSERT INTO employees VALUES (8, 'John', 'M');
My attempt is the following:
SELECT TOP 2 *
FROM employees
WHERE gender = 'F'
ORDER BY NEWID()
UNION
SELECT TOP 2 *
FROM employees
WHERE gender = 'M'
ORDER BY NEWID()
But it doesn't work since I can't put two order by in the same query.
Why not just use row_number()? One method without a subquery is:
SELECT TOP (4) WITH TIES e.*
FROM employees
WHERE gender IN ('M', 'F')
ORDER BY ROW_NUMBER() OVER (PARTITION BY gender ORDER BY newid());
This is slightly less performant than using ROW_NUMBER() in a subquery.
Or, a fun method would use APPLY:
select e.*
from (values ('M'), ('F')) v(gender) cross apply
(select top (2) e.*
from employees e
where e.gender = v.gender
order by newid()
) e;
You cannot put an ORDER BY in the combinable query (the first one) of the UNION. However, you can use ORDER BY if you convert each one into a table expression.
For example:
select *
from (
SELECT TOP 2 *
FROM employees
WHERE gender = 'F'
ORDER BY newid()
) x
UNION ALL
select *
from (
SELECT TOP 2 *
FROM employees
WHERE gender = 'M'
ORDER BY newid()
) y
Result:
id name gender
--- ----- ------
5 Dina F
4 Lola F
2 Jake M
3 Ryan M
See running example at SQL Fiddle.

How to SELECT record from a table where column is equal to a value but shouldn't be equal to another value?

I have listed two different solutions to the given problem. is there any better approach to solve these types of problems?
Q: GIVEN EMPLOYEE TABLE below, SELECT name whose id = 1 but not = 3
-- ID NAME
-- 1 ram
-- 2 shayam
-- 1 mohan
-- 7 mohan
-- 4 monu
-- 3 monu
-- 1 monu
-- 5 sonu
-- 1 sonu
-- 2 sonu
-- OUTPUT
-- mohan
-- ram
-- sonu
-- Solution 1:
SELECT DISTINCT(e1.NAME) FROM EMPLOYEE e1 JOIN EMPLOYEE e2 ON e1.name = e2.name WHERE e1.id = 1 AND e1.NAME NOT IN (
SELECT DISTINCT(e1.NAME) FROM EMPLOYEE e1 JOIN EMPLOYEE e2 ON e1.name = e2.name WHERE e2.id = 3);
-- Solution 2:
SELECT DISTINCT(e1.NAME) FROM EMPLOYEE e1 JOIN EMPLOYEE e2 ON e1.name = e2.name WHERE e1.id = 1
MINUS
SELECT DISTINCT(e1.NAME) FROM EMPLOYEE e1 JOIN EMPLOYEE e2 ON e1.name = e2.name WHERE e2.id = 3;
-- Use this code to test the logic:
CREATE TABLE EMPLOYEE( id INT, name VARCHAR(25) );
INSERT INTO EMPLOYEE(id, name) VALUES(1, 'ram');
INSERT INTO EMPLOYEE(id, name) VALUES(2, 'shayam');
INSERT INTO EMPLOYEE(id, name) VALUES(1, 'mohan');
INSERT INTO EMPLOYEE(id, name) VALUES(7, 'mohan');
INSERT INTO EMPLOYEE(id, name) VALUES(4, 'monu');
INSERT INTO EMPLOYEE(id, name) VALUES(3, 'monu');
INSERT INTO EMPLOYEE(id, name) VALUES(1, 'monu');
INSERT INTO EMPLOYEE(id, name) VALUES(5, 'sonu');
INSERT INTO EMPLOYEE(id, name) VALUES(1, 'sonu');
INSERT INTO EMPLOYEE(id, name) VALUES(2, 'sonu');
SELECT * FROM EMPLOYEE;
The minus is fine, but you don't need the select distinct. Minus is a set function that only returns distinct rows. I tend to use aggregation for this:
select e.name
from employee e
where id in (1, 3)
group by e.name
having max(id) = 1; -- there is no 3 if the max is 1
However, your methods are basically fine although I'll repeat that the select distincts are not necessary.

Get another value from row with max value with group by clause

Sorry for the title. I have this table:
CREATE TABLE people
(
Id int IDENTITY(1,1) PRIMARY KEY,
name varchar(50) NOT NULL,
FatherId int REFERENCES people(Id),
MotherId int REFERENCES people(Id),
age int NOT NULL
);
Which you can populate with these commands:
insert into people (name, age) VALUES ('Jonny', 50 )
insert into people (name, age) VALUES ('Angela', 48 )
insert into people (name, age) VALUES ('Donny', 55 )
insert into people (name, age) VALUES ('Amy', 55 )
insert into people (name, FatherId, MotherId, age) VALUES ('Marcus', 1, 2, 10)
insert into people (name, FatherId, MotherId, age) VALUES ('Beevis', 1, 2, 5)
insert into people (name, FatherId, MotherId, age) VALUES ('Stew', 3, 4, 24)
insert into people (name, FatherId, MotherId, age) VALUES ('Emily', 3, 4, 25)
My Goal
I want to get the age and name of the oldest child of each set of parents.
Getting just the age was pretty simple:
SELECT MAX(age) FROM people WHERE FatherId IS NOT NULL GROUP BY FatherId
But what if I want to get the age and their corresponding name?
I have tried
select p1.name, p1.age
FROM people p1
INNER JOIN
(
SELECT FatherId, MAX(age) age
FROM people
GROUP BY FatherId
) p2
ON p1.FatherId = p2.FatherId
but this just gives all the children because of the FatherId matching.
I can't seem to get the primary key (Id) because of the GROUP BY clause.
I suppose if this is not possible then some table restructuring may be required to make it possible?
EDIT
Here is a solution I found using CROSS APPLY
select child.name, child.age
FROM people parents
CROSS APPLY
(
select top 1 age, name
from people child
where child.FatherId = parents.Id
ORDER BY age DESC
) child
Here's a simple tweak of your own attempt. One possible advantage of doing it this way it to allow for ties.
select p1.name, p1.age
from people p1 inner join
(
select FatherId, max(age) max_age
from people
group by FatherId
) p2
on p2.FatherId = p1.FatherId and p2.max_age = p1.age;
Also you did refer to "set of parents" in the question. To do that you'd need to group by and join on MotherId as well, assuming of course that this matches up with the real world where children commonly have only a single parent in common.
You can try this query and see this demo
select
name,age
from
(select p.*, rn=row_number() over(partition by p.fatherid,p.motherid order by age desc) from
people p join
(select fatherid,motherid from people
where coalesce(fatherid,motherid) is not null
group by fatherid,motherid)t
on p.fatherid=t.fatherid and p.motherid=t.motherid
)t where rn=1
Two nested sub-select statements can be combined with inner join :
SELECT p1.age, p2.name
FROM
(
SELECT max(age) age
FROM people
WHERE FatherId is not null
GROUP BY FatherId ) p1
INNER JOIN
(
SELECT age, name
FROM people
WHERE FatherId is not null ) p2
ON ( p1.age = p2.age );
age name
--- ------
10 Marcus
25 Emily
SQL Fiddle Demo
This will pick up both parents
declare #t TABLE
(
Id int IDENTITY(1,1) PRIMARY KEY,
name varchar(50) NOT NULL,
FatherId int,
MotherId int,
age int NOT NULL
);
insert into #t (name, age) VALUES
('Jonny', 50 ),
('Angela', 48 ),
('Donny', 55 ),
('Amy', 55 );
insert into #t (name, FatherId, MotherId, age) VALUES
('Marcus', 1, 2, 10),
('Beevis', 1, 2, 5),
('Stew', 3, 4, 24),
('Emily', 3, 4, 25);
select tt.name, tt.age
, tt.fatherName, tt.fatherAge
, tt.motherName, tt.motherAge
from (
select ta.*
, tf.name as fatherName, tf.age as fatherAge
, tm.name as motherName, tm.age as motherAge
, row_number() over (partition by ta.FatherID, ta.MotherID order by ta.age desc) as rn
from #t ta
left join #t tf
on tf.id = ta.fatherID
left join #t tm
on tm.id = ta.motherID
) as tt
where FatherID is not null
and rn = 1

Max sal to print base on manager_id

I have this table:
id mgr_id sal
1 5 5000
2 5 6000
3 6 7000
4 6 8000
I expect this output:
id mgr_id sal MaX_sal
1 5 5000 6000
2 5 6000 6000
3 6 7000 8000
4 6 8000 8000
Based on mgr_id select max sal and print in front of id.
Thanks in advance.
Possible would be a subselect inside the select (although a bit slow as all subselects are):
select id, mgr_id, sal, (select max(sal) from mytable n2
where n2.mgr_id = mytable.mgr_id) as max_sal from mytable
Create a subtable grouping by mrg_id to get the max Sal per mrg_id and then left join it with the principal table on mrg_id -- Try this :
declare #table table (id int,mrg_id int,sal decimal(10,0))
insert into #table
select 1, 5, 5000 union all
select 2, 5, 6000 union all
select 3, 6, 7000 union all
select 4, 6, 8000
/***** change #table with your table name *****/
SELECT
t.id,
t.mrg_id,
t.sal,
t1.max_sal
FROM #table t
LEFT JOIN
(SELECT
mrg_id,
MAX(sal) max_sal
FROM #table
group by
mrg_id) t1
on t.mrg_id=t1.mrg_id
This would work even in SQL Server 6.5
SELECT id, mgr_id, sal,
(
SELECT MAX(sal)
FROM employees mgr
WHERE mgr.mgr_id = emp.mgr_id
) AS max_sal
FROM
employees emp
This works on SQL Server 2005:
WITH max_sals AS
(
SELECT mgr_id, MAX(sal) AS max_sal
FROM employees
GROUP BY mgr_id
)
SELECT emp.*, max_sals.max_sal
FROM
employees emp
LEFT JOIN max_sals
ON emp.mgr_id = max_sals.mgr_id
and this using partiton:
SELECT emp.*, MAX(emp.sal) OVER (PARTITION BY emp.mgr_id) AS max_sal
FROM employees emp
Sample data and table:
CREATE TABLE employees
(
id int,
mgr_id int,
sal int
)
INSERT INTO employees VALUES
(1,5,5000),
(2,5,6000),
(3,6,7000),
(4,6,8000)
With SQL Server 2012 simply use this query:
SELECT d.id, d.mgr_id, d.sal, MAX(sal) over(partition by mgr_id)
FROM #data d
You can use this query with SQL Server <2012:
SELECT d.id, d.mgr_id, d.sal, m.mx
FROM #data d
INNER JOIN (
SELECT mgr_id, mx = MAX(sal) FROM #data
GROUP BY mgr_id
) m
ON m.mgr_id = d.mgr_id;
Another option with SQL Server >= 2005:
SELECT d.id, d.mgr_id, d.sal, m.mx
FROM #data d
CROSS APPLY (SELECT mx = MAX(sal) FROM #data m WHERE m.mgr_id = d.mgr_id) m(mx)
Sample data:
Declare #data table([id] int, [mgr_id] int, [sal] int);
INSERT INTO #data([id], [mgr_id], [sal])
VALUES
(1, 5, 5000),
(2, 5, 6000),
(3, 6, 7000),
(4, 6, 8000)
;
Try Cross Apply,
SELECT id,
mgr_id,
sal,
Max_sal
FROM #your_table A
CROSS APPLY (SELECT Max(SAL) AS Max_sal
FROM #your_table B
WHERE B.mgr_id = A.mgr_id)cs

I want to make an insert using union all which has a column getting values from a sequence

I tried
INSERT INTO my_test_one (rollno,name, sirname, Dept)
(select rollno_seq.nextval,'name1','sirname1', Dept
FROM my_test_one_backup
WHERE dept = 500
UNION ALL
select rollno_seq.nextval,'name1','sirname1', Dept
FROM my_test_one_backup
WHERE dept = 501 );
While doing this I am getting the error
Error report:
SQL Error: ORA-02287: sequence number not allowed here
02287. 00000 - "sequence number not allowed here"
Don't use a UNION but a single SELECT and OR in this case:
SELECT rollno_seq.nextval,'name1','sirname1', Dept
FROM my_test_one_backup
WHERE dept = 500 OR dept = 501
Try:
INSERT INTO my_test_one
(rollno, name, sirname, Dept)
SELECT rollno_seq.nextval,
name1,
sirname1,
dept
FROM (select 'name1' as name1,'sirname1' as sirname1, Dept
FROM my_test_one_backup
WHERE dept = 500
UNION ALL
select 'name1','sirname1', Dept
FROM my_test_one_backup
WHERE dept = 501 );
Edit: Better still, use an OR like CodeBrickie says or and IN statement.
WHERE dept IN (500, 501);
Edit2:
Currently you are selecting 'name1', 'sirname1' as literals so each row returned will insert the next sequence number, 'name1', 'sirname1' and whatever the value of DEPT column is.
If your table has columns called name1 and sirname1 then you'll need to remove the single quotes (and you wouldn't need the column alias either) e.g.:
INSERT INTO my_test_one
(rollno, name, sirname, Dept)
SELECT rollno_seq.nextval,
name1,
sirname1,
dept
FROM (select name1, sirname1, Dept
FROM my_test_one_backup
WHERE dept = 500
UNION ALL
select name1, sirname1, Dept
FROM my_test_one_backup
WHERE dept = 501 );
Or
INSERT INTO my_test_one
(rollno, name, sirname, Dept)
SELECT rollno_seq.nextval,
name1,
sirname1,
dept
FROM my_test_one_backup
WHERE dept IN (500, 501);
You can't use a sequence in unioned selects, so you'll need to put the union in a sub-query and the sequence in the outer query:
INSERT INTO my_test_one (rollno,name, sirname, Dept)
select rollno_seq.nextval, name1, sirname1, dept
from (SELECT 'name1' as name1,'sirname1' as sirname1, Dept
FROM my_test_one_backup
WHERE dept = 500
UNION ALL
SELECT 'name1','sirname1', Dept
FROM my_test_one_backup
WHERE dept = 501 );
You should also note that, in SQL, double quotes indicate an object name and single quotes denote a string, so 'name1' and 'sirname1' will be static strings, not column references.