Raising salary by 15% using SQL only? - sql

I am trying to modify the SCOTT schema by raising employees salaries by 15%. If the resulting salary exceeds his or her highest possible salary, HISAL, in SALGRADE we just use HISAL, my code is:
select
coalesce((
select sal*1.15
from emp , salgrade
where
sal*1.15<=hisal
) ,
(select hisal from emp ,salgrade where sal*1.15>hisal )) raised_sal
from emp ;
The inner select returns multiple rows
Does anyone have suggestions or different code?

If we break-down what you're trying to do:
coalesce gives the first value, unless that is null then the second etc. You haven't specified any join condition, so you're doing a cartesian join between emp and salgrade. This will return the number of rows in emp multiplied by the number of rows in salgrade; subject to your join condition.
The second part of your query does exactly the same in the opposite direction.
Assuming a table_structure of:
create table emp (
emp_id number
, salary number -- current salary
, salgrade_id number -- current salary grade
, dept_id -- current department
);
create table salgrade (
, salgrade_id number
, hisal number -- max salary for that grade
);
You want to be doing something like this:
select least( e.salary * 1.15, s.hisal )
from emp e
join salgrade s
on e.salgrade_id = s.salgrade_id
The least function is similar to min; in that it will take the minimum value. However, it works over multiple columns on the same row, rather than multiple rows over the same column.
By taking the least of hisal and salary * 1.15 we ensure that if the salary increase is higher than hisal then hisal is taken instead.
In this case join means that for every emp_id we're taking the hisal for the appropriate salary grade. I would highly recommend reading up on SQL joins - this link was 7th on Google, which was a bit of a surprise ( Jeff seems to be everywhere ), but it's quite good and has many more links.
To change the query to enable you to do it for all employees earning over 100,000 ( the rich get richer ) it would look something like this:
select least( e.salary * 1.15, s.hisal )
from emp e
join salgrade s
on e.salgrade_id = s.salgrade_id
where e.salary > 100000
Or to complicate matters if you wanted to do it for only IT peoples ( why not ), you could extend it to something like this:
select least( e.salary * 1.15, s.hisal )
from emp e
join salgrade s
on e.salgrade_id = s.salgrade_id
join dept d
on e.dept_id = d.dept_id
where d.dept = 'IT'
These joins by being inner joins as opposed to outer joins ( normally left outer joins - see the link ) imply that every employee has a grade and a department.

You should join emp with salgrade through a non equijoin:
select sal*1.15
from
emp
inner join
salgrade sg
on emp.sal BETWEEN sg.losal AND sg.hisal <-- here!
where
sal*1.15<=hisal
More info: Introduction to Basic SQL, Part 3 - Complex Joins and Sub-queries:
"we joined using the BETWEEN operator. The paybands in the SALGRADE
table use a range of salary to designate a grade."
EDIT
To calculate the employee grade you should look for employee salary and look into salgrade for a salary range that contains employee salary.
This is the salgrade data:
...
INSERT INTO SALGRADE VALUES (2,1201,1400);
INSERT INTO SALGRADE VALUES (3,1401,2000);
...
If employee salary is 1495 that means that his grade is 2.
If you increase 1395 in 15% then this is out of range of your salrange and, as you explain, you should set salary at 1400 that is the hisal for range 2.
Your query:
select
coalesce((
select sal*1.15
from emp e2
inner join
salgrade sg
on e2.sal BETWEEN sg.losal AND sg.hisal
where
e2.EMPNO = e1.EMPNO and
sal*1.15<=hisal
) ,
(select hisal from emp e2 inner join salgrade sg
on e2.sal BETWEEN sg.losal AND sg.hisal
where e2.EMPNO = e1.EMPNO
)) raised_sal
from emp e1 ;
Without subqueries:
select
case
when sal*1.15 < hisal then sal*1.15
when sal*1.15 >= hisal then hisal
end
raised_sal
from
emp e1
inner join
salgrade sg
on e1.sal BETWEEN sg.losal AND sg.hisal
Disclaimer, not tested.

I think you missed only join condition ...
in both the query you can give join condition, if any (like emp.employee_name = salgrade.employee_name)
and have to give inner query condition also...
like
select
coalesce((
select sal*1.15
from emp e2, salgrade
where
sal*1.15<=hisal
and e2.employee_name = e1.employee_name
) ,
(select hisal from emp e3,salgrade where sal*1.15>hisal
and e3.employee_name = e1.emplyee_name
)) raised_sal
from emp e1;

Related

Need to retrieve columns from 3 tables

There are three tables: dept, emp, sal . You can find their structure and data in the images.
I need to extract the list of employees who have location as pune and have max salary in their department. Since there are five departments, the final output will contain five rows and columns of emp_id, dept, dept_id, salary.
I've tried...
select e.emp_id, dept,e.dept_id, max(sal) as 'highest salary'
from sal s,emp e,dept d
where e.emp_id = s.emp_id and d.dept_id = e.dept_id and loc ='Pune'
group by e.emp_id,e.dept_id,dept
order by e.dept_id
I would use apply :
select t.emp_id, d.dept, d.dept_id, t.sal
from dept d
cross apply ( select top 1 e.emp_id, s.sal as sal
from emp e
inner join sal s on s.emp_id = e.emp_id
where d.dept_id = e.dept_id and e.loc = 'Pune'
order by s.sal desc
) t;
Unless you have window functions (that depends on whether you're using MySQL, Oracle, SQLite, etc) you will need to do it in two steps.
Find the highest salary per department (for employees in 'Pune'), then another set of joins to find out who those people are.
SELECT
dep.dept,
dep.dept_id,
emp.emp_id,
sal.sal
FROM
emp
INNER JOIN
sal
ON sal.emp_id = emp.emp_id
INNER JOIN
(
SELECT
emp.dept_id,
MAX(sal.sal) AS max_sal
FROM
emp
INNER JOIN
sal
ON sal.emp_id = emp.emp_id
WHERE
emp.loc = 'Pune'
)
dep_sal
ON dep_sal.dept_id = emp.dept_id
AND dep_sal.max_sal = sal.sal
INNER JOIN
dep
ON dep.dept_id = emp.dept_id
WHERE
emp.loc = 'Pune'
ORDER BY
dep.dept,
emp.emp_id
EDIT: With SQL Server 2008 on-wards it's a bit easier...
WITH
emp_sal_ranked AS
(
SELECT
emp.dept_id,
emp.emp_id,
sal.sal,
RANK(sal.sal) OVER (PARTITION BY emp.dept_id
ORDER BY sal.sal
)
AS rank_sal
FROM
emp
INNER JOIN
sal
ON sal.emp_id = emp.emp_id
WHERE
emp.loc = 'Pune'
)
SELECT
dep.dept,
dep.dept_id,
emp_sal_ranked.emp_id,
emp_sal_ranked.sal
FROM
emp_sal_ranked
INNER JOIN
dept
ON dept.dept_id = emp_sal_ranked.dept_id
WHERE
emp_sal_ranked.rank_sal = 1
ORDER BY
dep.dept,
emp_sal_ranked.emp_id

How to count the number of members of different departments after executing a JOIN between two tables?

I have a table Emp and another table Dept
This is table Emp
This is table Dept
The query I have to execute is to display the average salary (i.e., sal) for all departments (i.e. DeptName) with more than 5 working people. So we'll have to do a JOIN or something and match the DeptID here with the DeptID there etc.
This is really confusing and I don't understand how to go about it.
you can use group by with having clause query for that.
group by Dept_id and check count using having clause.
SELECT
SUM(E.Sal) / COUNT(E.Dept_id) AS Avg_Sal,
E.Dept_Id
FROM EMP E
JOIN Dept D
ON D.DeptId = E.Dept_Id
GROUP BY E.DeptId
HAVING COUNT(E.Dept_Id) > 5
Select
DeptName,
AVG(sal) as averageSalary,
Count(EmpId) as NumberofEE
FROM Emp e
/*Left Join Because there are Nulls in the Dept_Id each Emp should belong to
1 dept*/
LEFT JOIN Dept d on e.Dept_Id = d.DeptId
GROUP BY
DeptName
Having Count(EmpId)> 5
This will give you the average salary (i.e., sal) for all departments (i.e. DeptName) with more than 5 working people
If i'm right with your requirement.

small tricky query on self join

I have a table EMP with columns as below:
create table emp(
empno number(4,0),
ename varchar2(10),
job varchar2(9),
mgr_id number(4,0),
sal number(7,2),
deptno number(2,0));
I want to list all employees' names along with their manager names, including those who do not have a manager. For those employees, their manager's name should be displayed as 'BOSS'.
The following query should work:
select e.ename, (case when m.ename is null then 'BOSS' else m.ename end) as mgrName
from emp e
left join emp m on m.empno = e.mgr_id
To my mind, the better solution is proposed by Charanjith.
In Oracle, we could even use NVL function instead of "case when" in order to replace null value by something. The result should be the same.
select e.ename empName, NVL(m.ename, 'BOSS') mgrName from emp e
left join emp m on m.empno = e.mgr_id
Moreover, we could see another solution : using inner join to filter on emp when a manager exists. Then union for all employees who don't have any manager.
select e.ename empName, m.ename mgrName from emp e inner join emp m on e.mgr_id = m.empno
union
select e.ename empName, 'BOSS' mgrName from emp e where not exists (select 1 from emp m where e.mgr_id = m.empno)
This work fine in oracle:
SELECT e.ename,
nvl(m.ename, 'BOSS')mgr
FROM emp a
LEFT JOIN emp b
ON m.empno = e.mgr_id;

Using WHERE clause on a joined table

I am trying to find the best way to filter out rows by using conditions based on a joined table. As an example I am joining the employees and salary grade table based on the salary grade for each employee. Then I want to show only the employees that have the same grade as a certain employee (Blake). I used the following code:
SELECT e.ename, e.sal, sg.grade
FROM emp e JOIN salgrade sg
ON(e.sal BETWEEN sg.losal AND sg.hisal)
WHERE sg.grade = (SELECT sg.grade FROM emp e JOIN salgrade sg ON(e.sal BETWEEN sg.losal AND sg.hisal) WHERE e.ename = 'BLAKE')
ORDER BY e.sal DESC
Is there a more optimal way to write the query?
Here is one method that uses window functions:
SELECT es.*
FROM (SELECT e.ename, e.sal, sg.grade,
MAX(CASE WHEN e.ename = 'BLAKE' THEN sg.grade END) OVER () as blake_grade
FROM emp e JOIN
salgrade sg
ON e.sal BETWEEN sg.losal AND sg.hisal
) es
WHERE grade = blake_grade
ORDER BY e.sal DESC;
I wouldn't use a join in the select in the WHERE clause; rather, I would use an inner scalar subquery to pick up BLAKE's salary and then an outer scalar subquery to pick up his salgrade. Otherwise very similar to your query:
select e.ename, e.sal, s.grade
from emp e inner join salgrade s on e.sal between s.losal and s.hisal
where s.grade = ( select grade
from salgrade
where (select sal from emp where ename = 'BLAKE')
between losal and hisal
)
order by sal desc
;
Using the same idea, you could do away with the first join as well (by returning the losal and hisal for BLAKE as well as his salgrade), but perhaps that is taking it too far.
If this is just about not having to write the same code twice, you can use a WITH clause:
WITH emps_and_sals AS
(
SELECT e.ename, e.sal, sg.grade
FROM emp e
JOIN salgrade sg ON e.sal BETWEEN sg.losal AND sg.hisal
)
SELECT *
FROM emps_and_sals
WHERE grade = (SELECT grade FROM emps_and_sals WHERE ename = 'BLAKE')
ORDER BY sal DESC;

multiple columns in ALL clause

Is it possible to multiple columns with ALL clause.
please provide some examples
i tried this correlated query on HR shema.
SELECT *
FROM job_history j
WHERE ( employee_id, job_id ) = ALL (SELECT employee_id, job_id
FROM employees e
WHERE e.employee_id = j.employee_id
AND e.job_id = j.job_id)
it does not consider about inner query and where conditions.Instead of that it returns job_history table.
Lets assume the tables
job_history
employee_id job_id
----------- ----------
102 IT_PROG,
101 AC_ACCOUNT
employees
employee_id job_id
----------- ----------
102 IT_PROG,
So according to my knowledge the query should output only 102 IT_PROG,
But its output is
employee_id job_id
----------- ----------
102 IT_PROG,
101 AC_ACCOUNT
thats the problem..
Oracle optimizer changes ANY, ALL and SOME operators during run time with EXISTS / NOT EXISTS and shifts the filtering criteria inside the correlated query. I checked one example on one of the site...
SELECT e1.empno, e1.sal
FROM emp e1
WHERE e1.sal > ALL (SELECT e2.sal
FROM emp e2
WHERE e2.deptno = 20);
is interpreted by the compiler as
SELECT e1.empno, e1.sal
FROM emp e1
WHERE NOT EXISTS (SELECT e2.sal
FROM emp e2
WHERE e2.deptno = 20
AND e1.sal <= e2.sal)
Still not very sure but may be the query that you have provided is being interpreted as following:-
Original Query:
SELECT *
FROM job_history j
WHERE ( employee_id, job_id ) = ALL (SELECT employee_id, job_id
FROM employees e
WHERE e.employee_id = j.employee_id
AND e.job_id = j.job_id)
Oracle Interpretation(just a guess work based on > ANY example)
SELECT *
FROM job_history j
WHERE NOT EXISTS (SELECT employee_id, job_id
FROM employees e
WHERE e.employee_id = j.employee_id
AND e.job_id = j.job_id
and e.employee_id != j.employee_id
AND e.job_id != j.job_id)
But still there is long way to discover the actual interpretation by ALL, ANY and SOME.
Anyways, there clauses were used long before I guess before 8i and so, now a days they are considered as obsolete, although Oracle supports them but they have been taken over by analitical functions and other techniques of querying data.
It must be like this:
SELECT *
FROM job_history j
WHERE ( employee_id, job_id ) =ANY (SELECT employee_id, job_id
FROM employees e
WHERE e.employee_id = j.employee_id
AND e.job_id = j.job_id)
Opposite of =ANY is <>ALL.
x =ANY (SELECT ...) is equal to x IN (SELECT ...),
x <>ALL (SELECT ...) is equal to x NOT IN (SELECT ...)
Then you may try this one:
SELECT *
FROM job_history j
JOIN employees e USING (employee_id, job_id)