SQL how can i detect if a value decrease over time? - sql

Hi, how can i check who are the employees whose salary has fallen ?
:
SELECT employees.emp_no, first_name, last_name, salary, from_date, to_date, hire_date
from employees
INNER JOIN salaries ON employees.emp_no = salaries.emp_no;
I only want to fetch the name of employees whose salary has fallen

You can use the positional analytic function LAG() to find these rows. This is a standard SQL function that peeks at a previous row, according to a specific criteria.
For example:
select emp_no, first_name, last_name
from (
select
e.*,
s.salary,
lag(s.salary) over(partition by e.emp_no order by from_date) as prev_salary
from employees e
join salaries s on s.emp_no = e.emp_no
) x
where salary < prev_salary

You should look into using windowing functions. It should look something like this:
with salary as (
SELECT employees.emp_no, concat(first_name, " ",last_name) as emp, salary, coalesce(hire_date, from_date) as from_date, to_date
from employees
INNER JOIN salaries ON employees.emp_no = salaries.emp_no
), last_sal as (
select emp_no, emp, salary, to_date, lag(salary) over (partition by emp_no, order by to_date) as last_salary
from salary
)
select *
from last_sal
where salary < last_salary
a windowing function basically takes a look at a subset of the data. In this case, the subset is of each employee, and then that window is ordered by to_date. Lag tells it to look backwards, and effectively produces a row which has the prior row's salary result next to the current row for the other columns.

Related

Analytic query trying to solve

im solving the following task with analytic functions and im stuck.
task: Write a query that shows the latest hired employee per department. In case of ties, use the lowest employee ID.
select a.EMPLOYEE_ID,
a.DEPARTMENT_ID,
a.FIRST_NAME,
a.LAST_NAME,
a.HIRE_DATE,
a.JOB_ID
from (select ROW_NUMBER() over (PARTITION by department_id order by hire_date desc)
from hr.EMPLOYEES a) A
where A = 1 ;
You need to include the columns you want to select in the outer query in the SELECT clause of the inner query and need to give an alias to the ROW_NUMBER computed value:
select EMPLOYEE_ID,
DEPARTMENT_ID,
FIRST_NAME,
LAST_NAME,
HIRE_DATE,
JOB_ID
from (
select EMPLOYEE_ID,
DEPARTMENT_ID,
FIRST_NAME,
LAST_NAME,
HIRE_DATE,
JOB_ID,
ROW_NUMBER() over (PARTITION by department_id order by hire_date desc) AS rn
from hr.EMPLOYEES
)
where rn = 1 ;
You still need to address the second part of the question:
In case of ties, use the lowest employee ID.
However, since this appears to be a homework question, I'll leave that for you to solve.

Analytic functions and means of window clause for calculating sum

I'm using Oracle and SQL Developer. I have downloaded HR schema and need to do some queries with it. Now I'm working with table Employees. As a user, I need the sum of salary of 3 employees with highest salary in each department. I have done query for defining 3 employees with highest salary in each department:
SELECT
*
FROM
(
SELECT
employee_id,
first_name
|| ' '
|| last_name,
department_id,
salary,
ROW_NUMBER()
OVER(PARTITION BY department_id
ORDER BY
salary DESC
--ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING
) result
FROM
employees
)
WHERE
result <= 3;
I need to use means of window clause. I have done something like this:
SELECT
department_id,
SUM(salary)
OVER (PARTITION BY department_id ORDER BY salary
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) total_sal
FROM
(
SELECT
employee_id,
first_name
|| ' '
|| last_name,
department_id,
salary,
ROW_NUMBER()
OVER(PARTITION BY department_id
ORDER BY
salary DESC
--ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING
) result
FROM
employees
)
WHERE
result <= 3;
Here is the result:
It has the necessary sum for 3 people in department and other unnnecessary results for 2 and so on. I need such result:
How can I modify my query to receive appropriate result (I need to use a window clause and analytic fuctions)?
You want aggregation rather than windowing in the outer query:
SELECT
department_id,
SUM(salary) total_sal
FROM
(
SELECT
employee_id,
first_name
|| ' '
|| last_name,
department_id,
salary,
ROW_NUMBER()
OVER(PARTITION BY department_id
ORDER BY
salary DESC
--ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING
) result
FROM
employees
) e
WHERE
result <= 3
GROUP BY department_id
I we were to do the same task with window functions only, then, starting from the existing query, we can either add another level of nesting of some sort, or use WITH TIES. Both pursue the same effect, which is to limit the results to one row per group.
The latter would look like:
SELECT
department_id,
SUM(salary) OVER(PARTITION BY department_id) total_sal
FROM (
SELECT e.*,
ROW_NUMBER() OVER(PARTITION BY department_id ORDER BY salary DESC) result
FROM employees e
) e
WHERE result <= 3
ORDER BY result FETCH FIRST ROW WITH TIES
While the former would phrase as:
SELECT department_id, total_sal
FROM (
SELECT e.*,
SUM(salary) OVER(PARTITION BY department_id) total_sal
FROM (
SELECT e.*,
ROW_NUMBER() OVER(PARTITION BY department_id ORDER BY salary DESC) result
FROM employees e
) e
WHERE result <= 3
) e
where result = 1

How to retrieve highest salary for each department across employees?

I am trying to compile a query which gives me the highest salary per each department and for each unique employee. The complexity is that 1 employee can be part of multiple departments.
In case the same employee has the highest salary in several departments, only the department with a lower salary should show. This is my start but I am not sure how to continue from here:
select max(salary) as salary, dd.dept_name,d.emp_no
from salaries s
inner join dept_emp d on
s.emp_no=d.emp_no
inner join departments dd on
d.dept_no=dd.dept_no
group by 2,3;
My output is:
What should I modify from here?
For an employee, you seem to only want to include the department with the smallest salary. I would recommend using window functions:
select s.*
from (select s.*,
rank() over (partition by dept_name order by salary desc) as seqnum_d
from (select s.*, d.dept_name,
rank() over (partition by dept_name order by salary) as seqnum_ed
from salaries s join
dept_emp d
on s.emp_no = d.emp_no join
departments dd
d.dept_no = dd.dept_no
) s
where seqnum_ed = 1
) s
where seqnum_d = 1;
Something like this?
select m.salary, m.emp_no, salary.dept_name from salary,
(select emp_no, min(salary) salary from salary group by emp_no) m
where
m.emp_no=salary.emp_no and m.salary=salary.salary;

Applying Aggregate Functions On all Columns

I have an Employee table with Salary. I want to list Salary - Avg(Salary) for every Employees. Can someone please help me with the SQL query for the same.
You can do this using window functions:
select e.*,
(salary - avg(salary) over ()) as diff
from employees e;
You could use an inline view to return a single row with the average salary. and join that row back to employee table.
Something like this:
SELECT e.emp_id
, e.salary
, e.salary - a.avg_salary
FROM employee e
CROSS
JOIN ( SELECT AVG(t.salary) AS avg_salary
FROM employee t
) a
ORDER BY e.emp_id
Just a couple other variations; the others already posted should serve the purpose quite well -- assuming we've all correctly inferred the desired output, given no DDL nor sample data, nor the expected output from the given inputs.
If there is no requirement to include the averaged salary [as the average over all rows] in addition to the calculated difference, then the following [shown with an optional casting to a decimal result] uses a scalar subselect to get the value to subtract from each employee salary:
select emp.*
, dec( salary - ( select avg(salary)
from employee_table )
, 11, 0 ) as saldif
from employee_table as emp
Or to use the averaged salaries in both the difference and as a column by itself, then again the scalar subselect, but made available for lateral reference in the [explicitly joined-to] subquery; again, optional casting for decimal results:
select x.*
from table
( select avg(salary)
from employee_table
) as a ( avgsal )
cross join lateral
( select emp.*
, dec( salary - avgsal , 11 ) as saldif
, dec( avgsal , 11 ) as salavg
from employee_table as emp
) as x
other solution ;)
with avgsalary as (
select avg(salary) avgsal from employee_table
)
select emp.*, case when emp.salary is null then cast(null as decimal) else round(emp.salary - avgs.avgsal, 2) end as diffsal, avgs.avgsal
from employee_table as emp cross join avgsalary avgs
And even another variation:
with EmpAvg (avgSalary)
as ( SELECT avg( salary ) from employee_table )
select e.*, a.avgSalary,
(e.salary - a.avgSalary) as diffAvg
from employee_table e cross join EmpAvg a
There may be many forms of queries that can give equivalent results. (Although I left off casting the calculated values, so not exactly equal result values.)

Employee that has a higher salary than the AVERAGE of his department - optimized

We have only a table named EMPLOYEESALARY in our database with the 3 following columns:
Employee_ID, Employee_Salary, Department_ID
Now I have to SELECT every employee that has a higher salary than the AVERAGE of his department. How do I do that?
I know this is a repeat question but the best solution I found everywere was:
SELECT * from employee join (SELECT AVG(employee_salary) as sal, department_ID
FROM employee GROUP BY Department_ID) as t1
ON employee.department_ID = t1.department_ID
where employee.employee_salary > t1.sal
Can we optimize it further and do it without a subquery?
Reference:
SELECT every employee that has a higher salary than the AVERAGE of his department
Employees with higher salary than their department average?
Find Schema here, to test: SQL Fiddle
Can we do it without a subquery?
Not that I can think of. Had the condition been >= then the following would have worked
SELECT TOP 1 WITH TIES *
FROM employee
ORDER BY CASE
WHEN employee_salary >= AVG(employee_salary)
OVER (
PARTITION BY Department_ID) THEN 0
ELSE 1
END
But this is not an optimisation and it won't work correctly for the > condition if no employee has a salary greater than the average anyway (i.e. all employees in a department had the same salary)
Can we optimize it further?
You could shorten the syntax a bit with
WITH T AS
(
SELECT *,
AVG(employee_salary) OVER (PARTITION BY Department_ID) AS sal
FROM employee
)
SELECT *
FROM T
WHERE employee_salary > sal
but it still has to do much the same work.
Assuming suitable indexes on the base table already exist then the only way of avoiding some more of that work at SELECT time would be to pre-calculate the grouped SUM and COUNT_BIG in an indexed view grouped by Department_ID (to allow the average to be cheaply derived) .
A more optimal form is likely to be:
select e.*
from (select e.*, avg(employee_salary) over (partition by department_id) as avgs
from employee e
) e
where employee_salary > avgs;
This (as well as other versions) can use an index on employee(department_id, employee_salary). The final where probably should not use an index, because it is selecting lots of rows.