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
Related
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
I already written query to find Superior manager of given employee using connect by prior,but I need to avoid Connect by Prior
Source Table:
Employee_Id Manager Id
1 10
10 20
20 Null
2 5
5 7
7 null
3 6
6 Null
OutPut table
Input Output
Employee_id Employee_ID
1 20
2 7
3 6
5 7
My Approach:
select * from (
SELECT *
FROM EMPLOYEEs
START WITH EMPLOYEE_ID = 103
CONNECT BY EMPLOYEE_ID = PRIOR MANAGER_ID
) where manager_id is null
Alternate Approach:
with cte (EMPLOYEE_ID,MANAGER_ID,lev) as (
select EMPLOYEE_ID, MANAGER_ID, 0 as lev
from employees
union all
select cte.EMPLOYEE_ID, employees.MANAGER_ID, lev + 1
from cte join
employees
on cte.MANAGER_ID = employees.EMPLOYEE_ID
)
select * from cte where employee_id=103 and MANAGER_ID is null;
but not getting expected output with alternate approach.
In your recursive subquery factoring, you need to identify the root employee_id and use that in your final query, like so:
WITH your_table AS
(SELECT 1 employee_id, 10 manager_id FROM dual UNION ALL
SELECT 10 employee_id, 20 manager_id FROM dual UNION ALL
SELECT 20 employee_id, NULL manager_id FROM dual UNION ALL
SELECT 2 employee_id, 5 manager_id FROM dual UNION ALL
SELECT 5 employee_id, 7 manager_id FROM dual UNION ALL
SELECT 7 employee_id, NULL manager_id FROM dual UNION ALL
SELECT 3 employee_id, 6 manager_id FROM dual UNION ALL
SELECT 6 employee_id, NULL manager_id FROM dual),
recursive(employee_id,
manager_id,
root_emp_id) AS
(SELECT employee_id,
manager_id,
employee_id root_emp_id
FROM your_table
WHERE manager_id IS NOT NULL
UNION ALL
SELECT yt.employee_id,
yt.manager_id,
r.root_emp_id
FROM recursive r
INNER JOIN your_table yt
ON r.manager_id = yt.employee_id)
SELECT root_emp_id employee_id,
employee_id ultimate_manager_id
FROM recursive
WHERE manager_id IS NULL
ORDER BY employee_id;
EMPLOYEE_ID ULTIMATE_MANAGER_ID
----------- -------------------
1 20
2 7
3 6
5 7
10 20
This mimics the connect_by_root function in connect by hierarchical queries.
Table:
create table temp_hierarchy_define (dept varchar2(25), parent_dept varchar2(25))
create table temp_employee (empid number(1), empname varchar2(50), dept varchar2(25), salary number(10))
Data
Select 'COMPANY' dept , 'COMPANY' parent_dept From Dual Union All
Select 'IT' , 'COMPANY' From Dual Union All
Select 'MARKET' , 'COMPANY' From Dual Union All
Select 'ITSEC' , 'IT' From Dual Union All
Select 'ITDBA' , 'IT' From Dual Union All
Select 'ITDBAORC' , 'ITDBA' From Dual Union All
Select 'ITDBASQL' , 'ITDBA' From Dual
select 1 empid, 'Rohan-ITDBASQL' empname ,'ITDBASQL' dept ,10 salary from dual union all
select 2, 'Raj-ITDBAORC' ,'ITDBAORC' ,20 from dual union all
select 3, 'Roy-ITDBA' ,'ITDBA' ,30 from dual union all
select 4, 'Ray-MARKET' ,'MARKET' ,40 from dual union all
select 5, 'Roopal-IT' ,'IT' ,50 from dual union all
select 6, 'Ramesh-ITSEC' ,'ITSEC' ,60 from dual
Requirement
Summarize salary of all IT dept:
CATEGORY SALARY
5,50
ITSEC,60
ITDBA,60
Summarize salary of all COMPANY dept:
CATEGORY SALARY
IT,170
MARKET,40
Summarize salary of all ITDBA dept:
CATEGORY SALARY
3,30
ITDBASQL,10
ITDBAORC,20
You will notice that we are trying to summarize based on the next level in the hierarchy. If any emp is already part of that level then we need to show the employee.
Trial Query:
Select Category,sum(salary) from (
Select
NVL((Select dept.dept from temp_hierarchy_define dept
Where dept.parent_dept = 'IT'
And dept.dept != 'IT'
Start With dept.dept = emp.dept
Connect by NOCYCLE dept.dept = Prior dept.parent_dept
and prior dept.dept is not null),emp.empid) category,
emp.*
From temp_employee emp
Where emp.DEPT in
(Select dept.dept from temp_hierarchy_define dept
Start With dept.dept = 'IT'
connect by nocycle prior dept.dept = dept.parent_dept) ) Group by Category
Concerns & queries:
Whether this query will work well in all scenarios. Or there any better way of doing it ??
How does where condition interact with connect by. For eg in the sub query we are filtering with parent_dept = 'IT', however while starting connect by some emp might have parent_dept = 'ITDBASQL' which is also part of IT. I am having a hard time in understanding the workflow.
Thank you for your time and assistance.
Or there any better way of doing it ?
This is an equivalent query that only requires one table scan for each table. You will need to determine whether your query or this one is more performant for your data/indexes/etc.
SQL Fiddle
Oracle 11g R2 Schema Setup:
create table temp_hierarchy_define (
dept varchar2(25), parent_dept varchar2(25));
create table temp_employee (
empid number(1), empname varchar2(50), dept varchar2(25), salary number(10));
INSERT INTO temp_hierarchy_define( dept, parent_dept )
Select 'COMPANY' , 'COMPANY' From Dual Union All
Select 'IT' , 'COMPANY' From Dual Union All
Select 'MARKET' , 'COMPANY' From Dual Union All
Select 'ITSEC' , 'IT' From Dual Union All
Select 'ITDBA' , 'IT' From Dual Union All
Select 'ITDBAORC' , 'ITDBA' From Dual Union All
Select 'ITDBASQL' , 'ITDBA' From Dual;
INSERT INTO temp_employee( empid, empname, dept, salary )
select 1, 'Rohan-ITDBASQL' ,'ITDBASQL' ,10 from dual union all
select 2, 'Raj-ITDBAORC' ,'ITDBAORC' ,20 from dual union all
select 3, 'Roy-ITDBA' ,'ITDBA' ,30 from dual union all
select 4, 'Ray-MARKET' ,'MARKET' ,40 from dual union all
select 5, 'Roopal-IT' ,'IT' ,50 from dual union all
select 6, 'Ramesh-ITSEC' ,'ITSEC' ,60 from dual;
Query 1:
SELECT dept,
SUM( salary )
FROM (
SELECT CASE
WHEN lvl = 1 AND h.parent_dept = e.dept
THEN CAST( e.empid AS VARCHAR2(25) )
ELSE root_dept
END AS dept,
e.empid,
e.salary
FROM ( SELECT CONNECT_BY_ROOT( dept ) AS root_dept,
h.*,
LEVEL AS lvl,
ROW_NUMBER() OVER ( PARTITION BY parent_dept ORDER BY ROWNUM ) AS rn
FROM temp_hierarchy_define h
WHERE parent_dept != dept
START WITH h.parent_dept = 'IT'
CONNECT BY NOCYCLE PRIOR h.dept = h.parent_dept
) h
LEFT OUTER JOIN
temp_employee e
ON ( h.dept = e.dept
OR ( h.parent_dept = e.dept AND h.lvl = 1 AND h.rn = 1)
)
)
GROUP BY dept
Results:
| DEPT | SUM(SALARY) |
|-------|-------------|
| ITDBA | 60 |
| 5 | 50 |
| ITSEC | 60 |
You can run the queries individually to find out what they are doing:
SELECT CONNECT_BY_ROOT( dept ) AS root_dept,
h.*,
LEVEL AS lvl,
ROW_NUMBER() OVER ( PARTITION BY parent_dept ORDER BY ROWNUM ) AS rn
FROM temp_hierarchy_define h
WHERE parent_dept != dept
START WITH h.parent_dept = 'IT'
CONNECT BY NOCYCLE PRIOR h.dept = h.parent_dept
Just lists all the rows in the hierarchy and uses CONNECT_BY_ROOT to get the department at the root of the branch of the hierarchy. LEVEL and ROW_NUMBER() are used to find the first row at the top of the hierarchy.
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 |
Suppose the company hierarchy is like this:
King
-> John
-> Jack
-> Chris
-> Sean
-> April
-> Jerry
-> Tom
Given an ancestor e.g. King, and a subordinate, e.g. Chris, is it possible to select all records along the path /King/John/Jack/Chris in one query? i.e. the query will return 4 records - King, John, Jack and Chris?
Table structure:
Employee_Name Employee_ID Manager_ID
Manager_ID references to Employee_ID
I am not sure what your table structure is. In case you store it as paths then the below should work. The query supports multiple records with Chris. It will select all of them.
with test as
(
select 1 id, '/King/John/Jack/Chris' str from dual union all
select 2 id, '/King/John/Jack/April' str from dual union all
select 3 id, '/King/John/Jack/Sean' str from dual union all
select 4 id, '/King/Jerry/Tom' str from dual
)
select id,
str,
regexp_substr (str, '[^/]+', 1, rn) split,
rn
from test
cross
join (select rownum rn
from (select max (length (regexp_replace (str, '[^/]+'))) + 1 mx
from test
)
connect by level <= mx
) A
where regexp_substr (str, '[^/]+', 1, rn) is not null
and instr(str, 'Chris') > 0
order by id, rn
;
Here is an example in SQL Fiddle
Example in SQL Fiddle
The trick is cross join that creates multiple rows for each row in main table.
ID STR SPLIT RN
1 /King/John/Jack/Chris King 1
1 /King/John/Jack/Chris John 2
1 /King/John/Jack/Chris Jack 3
1 /King/John/Jack/Chris Chris 4
with t as
(
select 'King' as Employee_Name, 1 as Employee_ID, -1 as Manager_ID from dual union all
select 'John' as Employee_Name, 2 as Employee_ID, 1 as Manager_ID from dual union all
select 'Jack' as Employee_Name, 3 as Employee_ID, 2 as Manager_ID from dual union all
select 'Chris' as Employee_Name, 4 as Employee_ID, 3 as Manager_ID from dual union all
select 'Sean' as Employee_Name, 5 as Employee_ID, 3 as Manager_ID from dual union all
select 'April' as Employee_Name, 6 as Employee_ID, 2 as Manager_ID from dual union all
select 'Jerry' as Employee_Name, 7 as Employee_ID, 1 as Manager_ID from dual union all
select 'Tom' as Employee_Name, 8 as Employee_ID, 7 as Manager_ID from dual
)
select Employee_Name
from t
connect by prior Manager_ID = Employee_ID
start with Employee_Name = 'Chris'
order by level desc