Select Query with Recursion - sql

I have two tables containing employee names (TableB) and Employee hierarchy( TableA) (manager_id from TableA can be employee_id in the same table ).
TableA
UniqueId Employee_ID Manager_ID
1 101 102
2 102 103
3 103 104
4 105 106
5 106 null
TableB
Employee_ID Employee_Name
101 First
102 Second
103 Third
104 Fourth
105 Fifth
106 Sixth
and I need output as below :
Employee_ID Employee_Name Transferred
101 First True
102 Second True
103 Third True
105 Fifth False
106 Sixth False
The Transferred column for each employee is calculated as =
isTransferred(Employee_ID)
{
If(Manager_ID is null) return false;
If(Manager_ID is present as employee_id in table A)
{
return isTransferred(manager_ID)
}
else
{
return true;
}
}
Is there any way to get the result in one select statement?

You can use a Recursive CTE and then get the last level of "recursion" for each employee. Once you have that, you just check the manager_id of that last level to find out if it's transferred.
For example:
with
tablea as (
select 1 as uniqueId, 101 as employee_id, 102 as manager_id from dual union all
select 2 as uniqueId, 102 as employee_id, 103 as manager_id from dual union all
select 3 as uniqueId, 103 as employee_id, 104 as manager_id from dual union all
select 4 as uniqueId, 105 as employee_id, 106 as manager_id from dual union all
select 5 as uniqueId ,106 as employee_id, null from dual
),
tableb as (
select 101 as employee_id, 'first' as employee_name from dual union all
select 102 as employee_id, 'second' as employee_name from dual union all
select 103 as employee_id, 'third' as employee_name from dual union all
select 104 as employee_id, 'fourth' as employee_name from dual union all
select 105 as employee_id, 'fifth' as employee_name from dual union all
select 106 as employee_id, 'sixth' as employee_name from dual
),
n (employee_id, employee_name, lvl, manager_id) as (
select b.employee_id, b.employee_name, 1, a.manager_id
from tablea a
join tableb b on a.employee_id = b.employee_id
union all
select
n.employee_id, n.employee_name, lvl + 1, a.manager_id
from n
join tablea a on a.employee_id = n.manager_id
),
m (employee_id, max_lvl) as (
select employee_id, max(lvl) from n group by employee_id
)
select n.employee_id, n.employee_name,
case when n.manager_id is not null then 'True' else 'False' end as transferred
from n
join m on n.employee_id = m.employee_id and n.lvl = m.max_lvl
order by n.employee_id
Result:
EMPLOYEE_ID EMPLOYEE_NAME TRANSFERRED
----------- ------------- -----------
101 first True
102 second True
103 third True
105 fifth False
106 sixth False

You can do it with a single pass through the tree as follows:
Outer join the management chain to the employees
Find the employees with no manager by mapping the unique_id to an "impossible" value, e.g. -1
Use the results of this join to walk down the tree, starting with the rows where the manager is null
Get the value for the expression in step 2 for the root rows; if this is the "impossible" value, then transfer => true
Filter out the rows for the impossible value
Which gives something like:
with table_a ( UniqueId, Employee_ID, Manager_ID ) as (
select 1, 101, 102 from dual union all
select 2, 102, 103 from dual union all
select 3, 103, 104 from dual union all
select 4, 105, 106 from dual union all
select 5, 106, null from dual
), Table_b ( Employee_ID, Employee_Name ) as (
select 101, 'First' from dual union all
select 102, 'Second' from dual union all
select 103, 'Third' from dual union all
select 104, 'Fourth' from dual union all
select 105, 'Fifth' from dual union all
select 106, 'Sixth' from dual
), rws as (
select b.*, a.Manager_ID,
nvl ( a.UniqueId, -1 ) transfer
from table_b b
left join table_a a
on b.Employee_ID = a.Employee_ID
)
select r.*,
case connect_by_root transfer
when -1 then 'true'
else 'false'
end transferred
from rws r
where transfer > 0
start with manager_id is null
connect by manager_id = prior employee_id;
EMPLOYEE_ID EMPLOYEE_NAME MANAGER_ID TRANSFER TRANSFERRED
103 Third 104 3 true
102 Second 103 2 true
101 First 102 1 true
106 Sixth <null> 5 false
105 Fifth 106 4 false

Related

Oracle - using connect by prior in Hierarchical Queries and put them into rows

I am new in Oracle, I am going to use connect by prior to implement the flat table instead of Hierarchical one. but I am a little bit confuse. My table is like this:
empTabl:
empID
empName
managerID
100
Sara
110
101
Ben
111
102
Alex
110
110
Ross
111
111
Mon
NULL
I am going to change the table like this(output):
emp
empName
subBoss
subBossName
Boss
BossName
100
Sara
110
Ross
111
Mon
101
Ben
111
Mon
NULL
NULL
102
Alex
110
Ross
111
Mon
110
Ross
111
Mon
NULL
NULL
111
Mon
NULL
NULL
NULL
NULL
You can use a recursive sub-query factoring clause for this:
WITH hierarchy (empID, empName, subBoss, subBossName, boss, bossName, depth, managerId) AS (
SELECT empID,
empName,
CAST(NULL AS NUMBER),
CAST(NULL AS VARCHAR2(20)),
CAST(NULL AS NUMBER),
CAST(NULL AS VARCHAR2(20)),
1,
managerID
FROM empTbl
UNION ALL
SELECT h.empID,
h.empName,
CASE h.depth
WHEN 1 THEN e.empID
ELSE h.subBoss
END,
CASE h.depth
WHEN 1 THEN e.empName
ELSE h.subBossName
END,
CASE h.depth
WHEN 2 THEN e.empID
ELSE h.boss
END,
CASE h.depth
WHEN 2 THEN e.empName
ELSE h.bossName
END,
h.depth + 1,
e.managerID
FROM hierarchy h
LEFT OUTER JOIN empTbl e
ON (h.managerID = e.empID)
WHERE depth < 3
)
CYCLE empID, depth SET is_cycle TO 1 DEFAULT 0
SELECT empID, empName, subBoss, subBossName, boss, bossName
FROM hierarchy
WHERE depth = 3;
Or, you can use a hierarchical query and pivot:
SELECT emp_id AS empID,
emp_name AS empName,
subboss_id AS subbossid,
subboss_name AS subbossname,
boss_id AS bossid,
boss_name AS bossname
FROM (
SELECT CONNECT_BY_ROOT(empID) AS root_empid,
empID,
empName,
LEVEL AS depth
FROM empTbl
WHERE LEVEL <= 3
CONNECT BY PRIOR managerID = empID
)
PIVOT (
MAX(empID) AS id,
MAX(empName) AS name
FOR depth IN (
1 AS emp,
2 AS subBoss,
3 AS boss
)
)
ORDER BY empid;
Which, for the sample data:
CREATE TABLE empTbl (empID, empName, managerID) AS
SELECT 100, 'Sara', 110 FROM DUAL UNION ALL
SELECT 101, 'Ben', 111 FROM DUAL UNION ALL
SELECT 102, 'Alex', 110 FROM DUAL UNION ALL
SELECT 110, 'Ross', 111 FROM DUAL UNION ALL
SELECT 111, 'Mon', NULL FROM DUAL;
Both output:
EMPID
EMPNAME
SUBBOSS
SUBBOSSNAME
BOSS
BOSSNAME
100
Sara
110
Ross
111
Mon
102
Alex
110
Ross
111
Mon
101
Ben
111
Mon
110
Ross
111
Mon
111
Mon
db<>fiddle here
You can also use connect by clause for that purpose.
select empID, empName
, subBoss, subBossName
, (select boss.empID from YourTable boss where boss.empID = subBossManagerID) Boss
, (select boss.empName from YourTable boss where boss.empID = subBossManagerID) BossName
from (
select empID, empName
, prior empID subBoss, prior empName subBossName
, prior t.managerID subBossManagerID
from YourTable t
start with managerID is null
connect by prior empID = managerID
)
order by 1
;
test here
The shorter version of MDO's answer would be -
WITH DATA AS (SELECT 100 AS empID, 'Sara' AS empName, 110 AS managerID FROM DUAL UNION ALL
SELECT 101, 'Ben', 111 FROM DUAL UNION ALL
SELECT 102, 'Alex', 110 FROM DUAL UNION ALL
SELECT 110, 'Ross', 111 FROM DUAL UNION ALL
SELECT 111, 'Mon', NULL FROM DUAL)
SELECT empID emp, empName, prior empID subBoss, prior empName subBossName
, prior t.managerID Boss
,CASE WHEN PRIOR t.managerID IS NOT NULL THEN CONNECT_BY_ROOT(t.empName) END AS BossName
FROM DATA T
START WITH managerID IS NULL
CONNECT BY PRIOR empID = managerID
ORDER BY 1;
Demo.
Ankit Bajpai's answer gives wrong name for manager after level 4 because of connect by root usage.
With small notification, this query gives shortest answer
WITH DATA AS (SELECT 100 AS empID, 'Sara' AS empName, 110 AS managerID FROM DUAL UNION ALL
SELECT 101, 'Ben', 111 FROM DUAL UNION ALL
SELECT 102, 'Alex', 110 FROM DUAL UNION ALL
SELECT 110, 'Ross', 111 FROM DUAL UNION ALL
SELECT 111, 'Mon', NULL FROM DUAL)
, b as( SELECT p.empID , p.empName, p.managerID, m.empName as managerName FROM DATA p left join data m
on ( p.managerID=m.empID))
SELECT empID emp, empName, prior empID subBoss, prior empName subBossName
, prior managerID Boss, prior managerName BossNAme
FROM b
START WITH managerID IS NULL
CONNECT BY PRIOR empID = managerID
ORDER BY 1;
fiddle

Total salary from a hierarchical sql query in Oracle

I am trying to find the total salary by using oracle hierarchical SQL query but I do not get the desired output.
I use Oracle Database 12c Enterprise Edition Release 12.1.0.2.0 - 64bit.
Below are the sample input tables and the hierarchical structure.
Below is the desired output table.
Below is the code I wrote but its not summing up at all levels.
SELECT COALESCE(e.Manager_id, e.Employee_id) Employee_id,
(SELECT Employee_name
FROM Employee_table
WHERE Employee_id = COALESCE(Manager_id, Employee_id)) Employee_name,
SUM(s.Employee_salary)
FROM Employee_table e
JOIN Salary_table s
ON s.Employee_id = e.Employee_id
WHERE CONNECT_BY_ISLEAF = 1
CONNECT BY PRIOR s.Manager_id = s.Employee_id
GROUP BY COALESCE(e.Manager_id, e.Employee_id)
ORDER BY SUM(s.Employee_salary) DESC;
What am I doing wrong here?
Here is on approach using a standard recursive query rather than Oracle specific connect by syntax:
with cte (employee_id, employee_name, child_id) as (
select employee_id, employee_name, employee_id from employee where manager_id is null
union all
select c.employee_id, c.employee_name, e.employee_id
from employee e
inner join cte c on e.manager_id = c.child_id
)
select c.employee_id, c.employee_name, sum(s.employee_salary) total_salary
from cte c
inner join salary s on s.employee_id = c.child_id
group by c.employee_id, c.employee_name
order by c.employee_id
The recursive query starts from employees that have no manager, and retrieves the children record of each node. Then, the outer query brings the salary table, and aggregate by "root employee".
Demo on DB Fiddle:
EMPLOYEE_ID | EMPLOYEE_NAME | TOTAL_SALARY
----------: | :------------ | -----------:
1 | John | 4000
2 | Philip | 17900
9 | Joe | 5700
with Salary_table (Employee_id, Employee_salary) as (
select 1, 4000 from dual union all
select 2, 2500 from dual union all
select 4, 3400 from dual union all
select 5, 4500 from dual union all
select 6, 4300 from dual union all
select 7, 2000 from dual union all
select 8, 1200 from dual union all
select 9, 3100 from dual union all
select 11, 2600 from dual
)
, Employee_table (Employee_id, Employee_name, Manager_id) as (
select 1, 'John', null from dual union all
select 2, 'Phil', null from dual union all
select 3, 'Rayan', 2 from dual union all
select 4, 'Peter', 2 from dual union all
select 5, 'Mark', 2 from dual union all
select 6, 'Steve', 3 from dual union all
select 7, 'Margret', 3 from dual union all
select 8, 'Paul', 3 from dual union all
select 9, 'Joe', null from dual union all
select 10, 'Bose', 9 from dual union all
select 11, 'Jane', 9 from dual
)
select mgr_id, mgr_name, sum(employee_salary) from (
select employee_id, connect_by_root employee_id mgr_id,
connect_by_root employee_name mgr_name
from
employee_table e
start with manager_id is null
connect by prior employee_id = manager_id
)
join salary_table
using(employee_id)
group by mgr_id, mgr_name
order by 1;
MGR_ID MGR_NAM SUM(EMPLOYEE_SALARY)
---------- ------- --------------------
1 John 4000
2 Phil 17900
9 Joe 5700

How to get distinct employees that do not have a particular skillset

I have a table that has two columns. Employee_id (which is unique per employee) and next column for employee skillset. One employee can have multiple skillset. How do I retrieve the list of distinct employees who don't have skillset 'c' if A,B,C,D,E are the five types of skillset that employees can have.
employee_id skillset
1 A
1 C
2 E
3 A
3 B
3 C
4 D
4 C
5 B
I have tried self join and other methods but it is not working.
select distinct employee_id from employee_skillset where skillset not like 'C'
When I run my query, it is still giving me employee_ids that have skillset of "c"
You can group by employee_id and set a condition in the HAVING clause:
select employee_id
from employee_skillset
group by employee_id
having sum(case when skillset = 'C' then 1 else 0 end) = 0
Or with NOT EXISTS:
select distinct s.employee_id
from employee_skillset s
where not exists (
select 1 from employee_skillset
where employee_id = s.employee_id and skillset = 'C'
)
What are your expected results from your data set? 2 and 5?
Why not something like below
SELECT DISTINCT employee_id
FROM Table1
WHERE skillset <> 'C';
MINUS set operator is one option:
SQL> with employee_skillset (employee_id, skillset) as
2 (select 1, 'a' from dual union all
3 select 1, 'c' from dual union all
4 select 2, 'e' from dual union all
5 select 3, 'a' from dual union all
6 select 3, 'b' from dual union all
7 select 3, 'c' from dual union all
8 select 4, 'd' from dual union all
9 select 4, 'c' from dual union all
10 select 5, 'b' from dual
11 )
12 select employee_id from employee_skillset
13 minus
14 select employee_id from employee_skillset where skillset = 'c';
EMPLOYEE_ID
-----------
2
5
SQL>
Yet another option:
<snip>
12 select employee_id
13 from (select employee_id,
14 case when skillset = 'c' then 1 else 0 end flag
15 from employee_skillset
16 )
17 group by employee_id
18 having sum(flag) = 0;
EMPLOYEE_ID
-----------
2
5
SQL>
Or:
<snip>
12 select employee_id
13 from (select employee_id,
14 listagg(skillset, ',') within group (order by null) lagg
15 from employee_skillset
16 group by employee_id
17 )
18 where instr(lagg, 'c') = 0;
EMPLOYEE_ID
-----------
2
5
SQL>

How to extract the number of subordinates from manager_id in the same table?

I've two columns: one with employee_id and one with manager_id
Apart from the President with employee_id 100 who doesn't have a manager (so manager_id is null) the rest have managers. For example, the President is the manager for two people with manager_id of 100. How to count and place it in this way:
employee_id 100 (column1) has 2 subordinates (column2)?
tried count, sum ,case, subquery and did't work
select employee_id,
manager_id,
first_name,
last_name,
case when employee_id = manager_id then count(employee_id) end,
count(manager_id)
from employees
--where manager_id is not null
group by manager_id,
employee_id,
first_name,
last_name
--having sum(manager_id) > 5
order by employee_id;
I expect to have 1st column as employee_id and second as the counted subordinates per employee_id.
Use a correlated hierarchical query:
Oracle Setup:
CREATE TABLE employees ( employee_id, manager_id ) AS
SELECT 100, NULL FROM DUAL UNION ALL
SELECT 101, 100 FROM DUAL UNION ALL
SELECT 102, 101 FROM DUAL UNION ALL
SELECT 103, 102 FROM DUAL UNION ALL
SELECT 104, 103 FROM DUAL UNION ALL
SELECT 105, 101 FROM DUAL UNION ALL
SELECT 106, 105 FROM DUAL UNION ALL
SELECT 107, 106 FROM DUAL UNION ALL
SELECT 108, 101 FROM DUAL UNION ALL
SELECT 109, 108 FROM DUAL;
Query:
SELECT employee_id,
(
SELECT COUNT(*)
FROM employees s
START WITH s.manager_id = e.employee_id
CONNECT BY PRIOR employee_id = manager_id
) AS num_subordinates
FROM employees e
Output:
EMPLOYEE_ID | NUM_SUBORDINATES
----------: | ---------------:
100 | 9
101 | 8
102 | 2
103 | 1
104 | 0
105 | 2
106 | 1
107 | 0
108 | 1
109 | 0
db<>fiddle here
if i understand your question, you could also do it with a simple Group by
this will count only the subordinates not the whole hierarchy
with tab as(
select 1 as emp_id, null as man_id from dual union all
select 2 as emp_id, 1 as man_id from dual union all
select 3 as emp_id, 1 as man_id from dual union all
select 2 as emp_id, null as man_id from dual union all
select 5 as emp_id, 2 as man_id from dual
)
select man_id as employee_id
, count(1) as cnt
from tab
where man_id is not null
group by man_id
EMPLOYEE_ID | CNT
----------: | --:
2 | 1
1 | 2
db<>fiddle here
Try this:
-- data preparation
WITH EMPS AS
(
SELECT 1001 AS EMP_ID, 'emp11' AS POS, 100 AS MGR_ID FROM DUAL UNION ALL
SELECT 1002 AS EMP_ID, 'emp12' AS POS, 100 AS MGR_ID FROM DUAL UNION ALL
SELECT 1003 AS EMP_ID, 'emp13' AS POS, 100 AS MGR_ID FROM DUAL UNION ALL
SELECT 2001 AS EMP_ID, 'emp21' AS POS, 200 AS MGR_ID FROM DUAL UNION ALL
SELECT 2002 AS EMP_ID, 'emp22' AS POS, 200 AS MGR_ID FROM DUAL UNION ALL
SELECT 100 AS EMP_ID, 'mgr1' AS POS, 1 AS MGR_ID FROM DUAL UNION ALL
SELECT 200 AS EMP_ID, 'mgr2' AS POS, 1 AS MGR_ID FROM DUAL UNION ALL
SELECT 1 AS EMP_ID, 'President' AS POS, NULL AS MGR_ID FROM DUAL )
-- Your actual query starts from here
SELECT
EE.EMP_ID,
EE.POS,
EE.MGR_ID,
CASE
WHEN EC.CNT IS NULL THEN 0
ELSE EC.CNT
END AS CNT
FROM
EMPS EE
LEFT JOIN (
SELECT
MGR_ID,
COUNT(1) AS CNT
FROM
EMPS
GROUP BY
MGR_ID
) EC ON EE.EMP_ID = EC.MGR_ID
ORDER BY
EE.EMP_ID;
Please add the other condition according to your needs.
DB Fiddle demo
Try this, use a select within the select
select emp.employee_id,
emp.manager_id,
emp.first_name,
emp.last_name,
(SELECT SUM(employees.employee_id) FROM employees where employees.manager_id=emp.employee_id) as subordinates,
count(manager_id)
from employees emp

Combine the result of one sql query into another

I have the below table.
CREATE TABLE Employee_id_credits ( Employee_id, credits ) AS
SELECT 10, 1 FROM DUAL UNION ALL
SELECT 12, 1 FROM DUAL UNION ALL
SELECT 10, 1 FROM DUAL UNION ALL
SELECT 12, 1 FROM DUAL UNION ALL
SELECT 12, 1 FROM DUAL UNION ALL
SELECT 14, 1 FROM DUAL;
The below query groups and counts the total number of credits for employees.
select Employee_id, count(*) as "Total_credits"
from Employee_id_credits
group by Employee_id;
Gives the below output.
Employee_id Total_credits
----------- -------------
10 2
12 3
14 1
I have a Employee Manager table with the hierarchy.
CREATE TABLE Employee_Manager ( Employee_id, Manager_id ) AS
SELECT 10, 101 FROM DUAL UNION ALL
SELECT 12, 120 FROM DUAL UNION ALL
SELECT 13, 120 FROM DUAL UNION ALL
SELECT 14, 150 FROM DUAL UNION ALL
SELECT 101, NULL FROM DUAL UNION ALL
SELECT 120, 130 FROM DUAL UNION ALL
SELECT 130, NULL FROM DUAL;
I have a query to find the top level manager of the employee.
SELECT
Employee_id
FROM
Employee_Manager
WHERE
Manager_id is null
CONNECT BY PRIOR
Manager_id = Employee_id
START WITH
Employee_id = '12';
I want to combine the above two queries so that the output would look like below. How do I combine both queries?
Manager Total_credits
------- -------------
101 2
130 3
150 1
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE Employee_id_credits ( Employee_id, credits ) AS
SELECT 10, 1 FROM DUAL UNION ALL
SELECT 12, 1 FROM DUAL UNION ALL
SELECT 10, 1 FROM DUAL UNION ALL
SELECT 12, 1 FROM DUAL UNION ALL
SELECT 12, 1 FROM DUAL UNION ALL
SELECT 14, 1 FROM DUAL;
CREATE TABLE Employee_Manager ( Employee_id, Manager_id ) AS
SELECT 10, 101 FROM DUAL UNION ALL
SELECT 12, 120 FROM DUAL UNION ALL
SELECT 13, 120 FROM DUAL UNION ALL
SELECT 14, 150 FROM DUAL UNION ALL
SELECT 101, NULL FROM DUAL UNION ALL
SELECT 120, 130 FROM DUAL UNION ALL
SELECT 130, NULL FROM DUAL;
Query 1 - Find the managers of each employee:
SELECT CONNECT_BY_ROOT( Employee_id ) AS Employee_id,
COALESCE( manager_id, employee_id ) AS manager_id
FROM Employee_manager
WHERE CONNECT_BY_ISLEAF = 1
CONNECT BY PRIOR Manager_id = Employee_id
Results:
| EMPLOYEE_ID | MANAGER_ID |
|-------------|------------|
| 10 | 101 |
| 12 | 130 |
| 13 | 130 |
| 14 | 150 |
| 101 | 101 |
| 120 | 130 |
| 130 | 130 |
Query 2 - Join that with the credits table and aggregate:
SELECT m.manager_id,
SUM( c.credits ) As total_credits
FROM Employee_id_credits c
INNER JOIN
(
SELECT CONNECT_BY_ROOT( Employee_id ) AS Employee_id,
COALESCE( manager_id, employee_id ) AS manager_id
FROM Employee_manager
WHERE CONNECT_BY_ISLEAF = 1
CONNECT BY PRIOR Manager_id = Employee_id
) m
ON ( c.employee_id = m.employee_id )
GROUP BY m.manager_id
Results:
| MANAGER_ID | TOTAL_CREDITS |
|------------|---------------|
| 101 | 2 |
| 130 | 3 |
| 150 | 1 |