Pro rata basis bonus distribution using setup table - sql

CREATE TABLE #empInfo(emp_id INT, dept_id INT, salary INT)
CREATE TABLE #empBonus(dep_id INT, emp_id INT, bonus INT)
I have above two tables for Employee and Bonus, where I will allocate bonus for the employees in bonus table every year but in example we are going to do it for a year only so the year column is not given.
INSERT INTO #empInfo VALUES
(111, 100, 5000),
(112, 100, 4000),
(113, 100, 4000),
(114, 100, 3500),
(115, 100, 4500),
(116, 100, 3000),
(114, 200, 3500),
(115, 200, 4500),
(116, 200, 3000),
(114, 300, 3500),
(115, 300, 3500),
(116, 300, 3500)
INSERT INTO #empBonus VALUES
(100, 111, 1000),
(100, NULL, 4000),
(100, 111, 500),
(100, NULL, 4000),
(100, 113, 700),
(200, 114, 600),
(200, NULL, 1600),
(300, 116, 900)
Above, If employee id defined in empBonus table then the bonus should allocated for that employee and if null that means bonus for all employees whose are not listed in the empBonus and will get bonus according to their salary.
we can define bonus for multiple employees and it can be multiple for same employee, in this case we have to sum total bonus and perform operation accordingly. Same case is for NULL.
For example, Base on formula given below, I have done below calculation in the EXCEL for easy understanding and in SQL I am trying with OUTER APPLY but not getting what I want from single query ?
--Formula = bonus*salary/totSalary(of respective group or employee)
DeptID EmpID TotBonus Salary TotSalary Bonus
100 111 1500 5000 5000 1500.00000000000
100 112 8000 4000 15000 2133.33333333333
100 113 700 4000 4000 700.00000000000
100 114 8000 3500 15000 1866.66666666666
100 115 8000 4500 15000 2400.00000000000
100 116 8000 3000 15000 1600.00000000000
200 114 600 3500 3500 600.00000000000
200 115 1600 4500 7500 960.00000000000
200 116 1600 3000 7500 640.00000000000
300 114 0 3500 7000 0.00000000000
300 115 0 3500 7000 0.00000000000
300 116 900 3500 3500 900.00000000000
Any help will be appreciated, thanks in advance :)

Here is one way using FULL OUTER JOIN and SUM() OVER() Window aggregate
;WITH cte
AS (SELECT ei.emp_id,ei.dept_id, eb.dep_id,
bonus = COALESCE(bonus, Max(CASE WHEN eb.emp_id IS NULL THEN bonus END)
OVER( partition BY COALESCE(ei.dept_id, eb.dep_id) )),
salary = Cast(salary AS NUMERIC(22, 6)),
TotSalary= Iif(eb.emp_id IS NULL, Sum(CASE WHEN eb.emp_id IS NULL THEN salary END)
OVER(partition by ei.dept_id), salary)
FROM #empInfo ei
FULL OUTER JOIN (SELECT bonus= Sum(bonus),
dep_id,
emp_id
FROM #empBonus
GROUP BY dep_id,
emp_id) eb
ON ei.dept_id = eb.dep_id
AND eb.emp_id = ei.emp_id)
SELECT emp_id,
bonus,
salary,
TotSalary,
( bonus * salary ) / NULLIF(TotSalary, 0)
FROM cte
WHERE emp_id IS NOT NULL
Result:
+--------+-------+-------------+-----------+--------------------+
| emp_id | bonus | salary | TotSalary | Bonus Distribution |
+--------+-------+-------------+-----------+--------------------+
| 111 | 1500 | 5000.000000 | 5000 | 1500.00000000000 |
| 112 | 8000 | 4000.000000 | 19000 | 1684.21052631578 |
| 113 | 8000 | 4000.000000 | 19000 | 1684.21052631578 |
| 114 | 8000 | 3500.000000 | 19000 | 1473.68421052631 |
| 115 | 8000 | 4500.000000 | 19000 | 1894.73684210526 |
| 116 | 8000 | 3000.000000 | 19000 | 1263.15789473684 |
+--------+-------+-------------+-----------+--------------------+

Well, that was a good challenge for me.
First, create a cte is to calculate the TotSalary column:
;With cteTotalSalary as
(
-- select total salary for employees that are in the bonus table
SELECT e.emp_id, dept_id, Salary, Salary As TotSalary
FROM #empInfo e
INNER JOIN #empBonus b ON e.dept_id = b.dep_id AND e.emp_id = b.emp_id
UNION
-- select total salary for employees that are in NOT the bonus table
SELECT e.emp_id, dept_id, Salary, SUM(Salary) OVER(PARTITION BY dept_id) As TotSalary
FROM #empInfo e
WHERE EXISTS (
SELECT 1
FROM #empBonus b
WHERE e.dept_id = b.dep_id
AND b.emp_id IS NULL
)
AND NOT EXISTS
(
SELECT 1
FROM #empBonus b
WHERE e.dept_id = b.dep_id
AND e.emp_id = b.emp_id
)
)
Then, query this cte twice with a union to get both types of bonuses (employee bonus and department bonus)
-- Get the bonus of the employess that exists in the empBonus table
SELECT c.emp_id, dept_id, SUM(Bonus) OVER(PARTITION BY c.emp_id) as Bonus, Salary, TotSalary, CAST(SUM(CAST(Bonus as decimal)) OVER(PARTITION BY c.emp_id) as decimal) as [Bonus Distribution]
FROM cteTotalSalary c
INNER JOIN #empBonus b ON c.dept_id = b.dep_id AND c.emp_id = b.emp_id
UNION
-- Get the bonus of the employees that does not exists in the empBonus table
SELECT c.emp_id, dept_id, SUM(Bonus) OVER(PARTITION BY c.emp_id), Salary, TotSalary, SUM(CAST(Bonus as decimal) * Salary / TotSalary) OVER(PARTITION BY c.emp_id)
FROM cteTotalSalary c
INNER JOIN #empBonus b ON c.dept_id = b.dep_id AND b.emp_id IS NULL
AND NOT EXISTS (
SELECT 1
FROM #empBonus b
WHERE c.dept_id = b.dep_id
AND c.emp_id = b.emp_id
)
Results:
emp_id dept_id Bonus Salary TotSalary Bonus Distribution
111 100 1500 5000 5000 1500.000000000
112 100 8000 4000 19000 1684.210526314
113 100 8000 4000 19000 1684.210526314
114 100 8000 3500 19000 1473.684210526
115 100 8000 4500 19000 1894.736842104
116 100 8000 3000 19000 1263.157894736
You can see it in action here

Related

Find the average value from the previous row and the current row for a same department id

I have 3 employees with the same departments. i wanted to find the average of their salaries based on the previous rows.
For example
Employee_id department_id salary avg(salary)
101 1 5000 5000
102 1 10000 7500
103 1 15000 10000
This is like for department id as 1 with first salary we find the average as 5000 for employee id 101 and for the same department id as 1 for the employee id as 102 , we find the average for the 2 values grouped by department id.
Hence
average is (10000+5000) /2 = 7500
But for the employee id as 103, department id is 1 and is grouped with all the above three values of amount.
Hence,
average of salary is (10000+5000+15000)/3 = 10000
The requirement is i have been asked to use query_partition_clause and order_by_clause.
Hence i tried as follows,
select avg(salary) OVER (partition by department_id ORDER BY department_id ) salary, department_id, salary from employee
But i am always getting the values by considering the department of 3 data values.
Henceforth can somebody help on this resolution?
Many Thanks for the help.
Use ORDER BY salary (or ORDER BY employee_id) rather than ORDER BY department_id:
Oracle Setup:
CREATE TABLE employees ( Employee_id, department_id, salary ) AS
SELECT 101, 1, 5000 FROM DUAL UNION ALL
SELECT 102, 1, 10000 FROM DUAL UNION ALL
SELECT 103, 1, 15000 FROM DUAL;
Query:
SELECT e.*,
AVG( salary ) OVER ( PARTITION BY department_id ORDER BY salary ) AS avg_salary
FROM employees e
Output:
EMPLOYEE_ID | DEPARTMENT_ID | SALARY | AVG_SALARY
----------: | ------------: | -----: | ---------:
101 | 1 | 5000 | 5000
102 | 1 | 10000 | 7500
103 | 1 | 15000 | 10000
db<>fiddle here
SELECT EMPLOYEE_ID,
DEPARTMENT_ID,
SALARY,
(SELECT AVG(SALARY)
FROM EMPLOYEES B
WHERE B.EMPLOYEE_ID <= A.EMPLOYEE_ID) AVG_SALARY
FROM EMPLOYEES A
GROUP BY EMPLOYEE_ID,
DEPARTMENT_ID,
SALARY
A sub query can be done in the query itself by filtering the employee ID. I hope I have helped with something.

Select rows where every child row meets a condition

In my Oracle DB, I have two tables in a one-to-many relationship: Managers and Employees.
+------------+-------+------------+
| Manager_ID | Name | Department |
+------------+-------+------------+
| 1 | Steve | Sales |
| 2 | Ben | Sales |
| 3 | Molly | Accounts |
+------------+-------+------------+
+-------------+------------+--------+-----+
| Employee_ID | Manager_ID | Name | Age |
+-------------+------------+--------+-----+
| 1 | 1 | Kyle | 25 |
| 2 | 1 | Gary | 31 |
| 3 | 2 | Renee | 31 |
| 4 | 2 | Oliver | 32 |
+-------------+------------+--------+-----+
How do I select only those Managers where every one of his Employees is over the age of 30?
In my example data, the only Manager who meets this condition is Ben, because both of his employees are over 30.
I thought something like this would do it, but it's wrong:
SELECT m.manager_id
FROM managers m
WHERE m.manager_id IN (SELECT e.manager_id
FROM employees e
GROUP BY e.manager_id
HAVING e.age > 30)
Use not exists :
select m.*
from manager m
where not exists (select 1
from Employees e
where e.Manager_ID = m.Manager_ID and e.Age < 30
) and
exists (select 1 from Employees e where e.Manager_ID = m.Manager_ID)
The only thing I don't like about Yogesh's answer (which I upvoted, since it's probably the way I'd write it) is that you have to go to the employees table a second time, to make sure the manager actually has at least one employee.
On the plus side, the NOT EXISTS that Yogesh used will allow Oracle to stop looking at a manager's employees once it finds one that is too young. So, maybe it's a toss-up.
I'll offer this alternative. It is shorter than the NOT EXISTS and does not have to go to the employees table a second time.
SELECT m.*
FROM manager m
CROSS APPLY (
SELECT min(age) min_age
FROM employee e
WHERE e.manager_id = m.manager_id ) ma
where ma.min_age >= 30;
Using sub-query for counts
SQL> WITH manager(Manager_ID, Name, Department) AS (
2 SELECT 1, 'Steve', 'Sales' FROM dual UNION ALL
3 SELECT 2, 'Ben', 'Sales' FROM dual UNION ALL
4 SELECT 3, 'Molly', 'Accounts' FROM dual),
5 employee(Employee_ID, Manager_ID, Name, Age) AS (
6 SELECT 1 , 1, 'Kyle', 25 FROM dual UNION ALL
7 SELECT 2 ,1, 'Gary', 31 FROM dual UNION ALL
8 SELECT 3, 2, 'Renee', 31 FROM dual UNION ALL
9 SELECT 4, 2 , 'Oliver', 32 FROM dual)
10 ---------------------------
11 --- End of data preparation
12 ---------------------------
13 SELECT m.name
14 FROM manager m
15 JOIN (SELECT manager_id,
16 COUNT(1) total,
17 COUNT(CASE WHEN age > 30 THEN 1 ELSE NULL END) age_30_above
18 FROM employee
19 GROUP BY manager_id) ee
20 ON m.manager_id = ee.manager_id
21 WHERE total = age_30_above;
Output
NAME
-----
Ben
Your query will be:
SELECT m.name
FROM manager m
JOIN (SELECT manager_id,
COUNT(1) total,
COUNT(CASE WHEN age > 30 THEN 1 ELSE NULL END) age_30_above
FROM employee
GROUP BY manager_id) ee
ON m.manager_id = ee.manager_id
WHERE total = age_30_above;
SELECT manager_id
FROM employees -- managers
minus
select manager_id
from employees
where age <= 30
You can use ALL function like this:
SELECT m.manager_id
FROM managers m
WHERE (30 <= ALL (SELECT e.age FROM employees e WHERE e.manager_id = m.manager_id));
You might want to reverse the conditions, select all managers, who dont have any employee below 30
select * from managers
where manager_id not in (select manager_id
from employees
where age < 30)

How to Improve This Self-Joins

I am learning Oracle SQL by working with its primitive HR schema where there is EMPLOYEES table which has three columns that I'm mainly interested in: MANAGER_ID, which is basically a self reference to EMPLOYEES.EMPLOYEE_ID, DEPARTMENT_ID, and SALARY. (You can find the schema diagram and schema objects here).
I wish, for each employee, to retrieve his/her SALARY, alongside of employee's manager's departmental average salary. For instance, if we have the following (EMPLOYEE_ID = 140 is the interested party here):
+-------------+--------+---------------+------------+
| EMPLOYEE_ID | SALARY | DEPARTMENT_ID | MANAGER_ID |
+-------------+--------+---------------+------------+
| 140 | 12000 | 50 | 110 |
| 110 | 20000 | 60 | 101 |
| 156 | 18000 | 60 | 101 |
| 175 | 15000 | 60 | 105 |
| 320 | 24000 | 60 | 105 |
+-------------+--------+---------------+------------+
I am interested in obtaining an average salary of all the managers (not all other non-managerial employees) in department where employee's manager works at (in this case, DEPARTMENT_ID =60), and compare it with employee's (in this case, 140). In a sample data above, the output should be:
+-------------+--------+-------------+-------------+------------+
| EMPLOYEE_ID | SALARY | AVG_MGR_SAL | MGR_DEPT_ID | MANAGER_ID |
+-------------+--------+-------------+-------------+------------+
| 140 | 12000 | 19250 | 60 | 110 |
+-------------+--------+-------------+-------------+------------+
where we have four (4) managers working in department 60, and $19250 being calculated as (20000 + 18000 + 15000 + 24000) / 4. I have come up with the following query that seems to work (and excludes those employees that don't have a manager):
select
employee_id
, salary employee_salary
, trunc(mgr_info.avg_manager_salary_per_dept, 0) emp_manager_avg_sal_dept
, mgr_info.manager_dept_id
, mgr_info.manager_id
from employees
join (
select
e1.employee_id manager_id
, e1.department_id manager_dept_id
, e1.salary manager_salary
, avg(e1.salary) over (partition by e1.department_id) avg_manager_salary_per_dept
from employees e1
join (
select distinct manager_id
from employees
where manager_id is not null
) mgr_ids
on e1.employee_id = mgr_ids.manager_id
) mgr_info
on employees.manager_id = mgr_info.manager_id
order by employee_id
However, I feel like that there should be a better way of getting the same result with fewer self-joins. Is there a way to get a better performance?
Something like this... You only need one join, you can compute the average salary for the manager's department on the "manager" copy of the table. I only included a few columns, you may need more, or fewer, but I believe the core of what you wanted is covered.
(NOTE: Edited since I realized I missed one detail in the requirement)
select e.employee_id as employee_id,
e.salary as employee_salary,
m.employee_id as manager_id,
m.department_id as manager_dept_id,
m.avg_salary as avg_sal_of_mgr_dept
from hr.employees e inner join
( select employee_id, department_id,
avg(salary) over (partition by department_id) as avg_salary
from hr.employees
where employee_id in (select manager_id from hr.employees)
) m
on e.manager_id = m.employee_id
;
Here is an option which uses a series of joins to get your result:
SELECT DISTINCT t1.EMPLOYEE_ID,
t1.SALARY,
t1.DEPARTMENT_ID,
COALESCE(t2.SALARY, 0.0) AS ManagerAvgSal
FROM employees t1
LEFT JOIN
(
SELECT e1.DEPARTMENT_ID, AVG(e1.SALARY) AS SALARY
FROM employees e1
WHERE e1.EMPLOYEE_ID IN (SELECT DISTINCT MANAGER_ID FROM employees)
GROUP BY e1.DEPARTMENT_ID
) t2
ON t1.DEPARTMENT_ID = t2.DEPARTMENT_ID

Need SQL Query for the following data and Condition

I have a table with Emp_salary name.
Table data
id name Slry
1 Ram 1500
2 janak 500
3 Anuj 400
4 Hardik 2000
5 Amit 2500
6 Atul 3000
7 Rahul 3500
8 Jay 4000
9 Parth 1500
10 Lalit 600
11 Ramesh 5000
My Que is : I will pass name of Emp in where condition.
And I want O/P like all Emp will come out that’s Salary will Less than that Emp’s salary(That I was pass in where condition)
Ex.
If i pass Lalit if i pass Atul
O/P will be like O/P will be like
name Slry name Slry
janak 500 janak 500
Anuj 400 Anuj 400
Lalit 600 Lalit 600
Ram 1500
Hardik 2000
Amit 2500
Atul 3000
Parth 1500
Don’t use “Sub Query” or “Function”
I need only Simple SQL Query for that.
You can use non-equality join:
WITH Src AS
(
SELECT * FROM (VALUES
(1 , 'Ram ', 1500),
(2 , 'janak ', 500 ),
(3 , 'Anuj ', 400 ),
(4 , 'Hardik', 2000),
(5 , 'Amit ', 2500),
(6 , 'Atul ', 3000),
(7 , 'Rahul ', 3500),
(8 , 'Jay ', 4000),
(9 , 'Parth ', 1500),
(10, 'Lalit ', 600 ),
(11, 'Ramesh', 5000)
)T(id, name, Slry)
)
SELECT S1.name, S1.Slry
FROM Src S1
JOIN Src S2 ON S1.Slry<=S2.Slry
WHERE S2.name='Lalit'
The best approach is to use a CTE to determine reference salary and then get only employees having a smaller salary than this salary by using a non-equality join.
DECLARE #empName as NVARCHAR(100) = 'Atul';
WITH cteRefEmp
AS
(
SELECT slry
FROM tmp.Emp
WHERE name = #empName
)
SELECT E.name, E.slry
FROM
tmp.Emp E
INNER JOIN cteRefEmp RE ON E.slry <= RE.slry
I found in this way also :
SELECT emp.name , emp.slry
from emp
WHERE emp.slry <= (SELECT slry FROM emp WHERE emp.name = 'Lalit')

Select statement with multiple rows from condition on values in single column affecting more than one column

I have the table below.Using salary as condition I want to get multiple rows. Below is current table call it employee.
empid | name | salary
-----------------------------------
1 A1 alex 20000
2 B2 ben 4500
3 C1 carl 14000
compare the salary to certain fixed values, and every time the salary is larger than the fixed value, show a record in output.Also output a calculated tax column.Below is a step closer to my final result
select e.*,tax= (case when salary<6000 then tax=0.06 *salary,when salary between 6000 and 18000 then tax= 0.06 *(salary -6000),else tax =0 ),m.incometype,
from employee e
left join
(
select 0 as threshold, 101 as incometype
union
select 5999 as threshold, 102 as incometype
union
select 17999 as threshold, 103 as incometype
) m
on e.salary > m.threshold
order by e.empid
Desired ouput would be:
empid | name | salary | incometype | tax
----------------------------------------------
1 A1 alex 20000 101 360
2 A1 alex 20000 102 720
3 A! alex 20000 103 0
4 B2 ben 4500 101 270
5 C1 carl 14000 101 360
6 C1 carl 14000 102 480
this is a question further to Select statement with multiple rows from condition on values in single column
this may help you
select e.*,
case
when salary<6000 then (0.06*salary)
when salary between 6000 and 1800 then (0.06*(salary -6000))
when m.incometype=103 then 0
end as tax
,m.incometype,
from employee e
left join
(
select 0 as threshold, 101 as incometype
union
select 5999 as threshold, 102 as incometype
union
select 17999 as threshold, 103 as incometype
) m
on e.salary > m.threshold
order by e.empid